Java核心技術 Java SE 8的流庫2

9.收集到映射表中

Collectors.toMap方法有兩個函數引元,用來產生映射表的鍵和值:

Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName));

通常情況下,值應該是實際的元素,因此第二個函數可以使用Function.identity()

多個元素具有相同的鍵,收集器會拋出一個IllegalStateException對象。可以通過提供第三個函數引元來覆蓋這種行爲。該函數會針對給定的已有值和新值來解決衝突並確定鍵對應的值。這個函數應該返回已有值、新值或它們的組合。
獲取所有Locale的名字爲鍵和其本地化名字爲值:

Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
Map<String, String> languageNames = locales.collect(
        Collectors.toMap(
                Locale::getDisplayLanguage,
                l -> l.getDisplayLanguage(l),
                (existingValue, newValue) -> existingValue));

不必擔心同一種語言是否可能出現兩次,只記錄第一項。

如果需要了解給定所有國家的語言,就需要一個Map< String, Set< String > >:

Map<String, Set<String>> countryLanguageSets = locales.collect(
        Collectors.toMap(
                Locale::getDisplayCountry,
                l -> Collections.singleton(l.getDisplayLanguage()),
                (a, b) -> {
                    Set<String> union = new HashSet<>(a);
                    union.addAll(b);
                    return union;
                }
        ));

想要得到TreeMap,可以將構造器做爲第4個引元來提供,必須提供一種合併函數:

Map<Integer, Person> idToPerson = people.collect(
        Collectors.toMap(
                Person::getId,
                Function.identity(),
                (existingValue, newValue) -> { throw new IllegalStateException(); },
                TreeMap::new));

對於每個toMap方法,都有一個等價的可以產生併發散列表的toConcurrentMap方法。單個併發映射表可以用於併發集合處理。當使用並行流時,共享的映射表比合並映射表要高效。元素不再是按照流中的順序收集。

public class CollectingIntoMaps {
    public static class Person{
        private int id;
        private String name;
        public Person(int id, String name) {
            this.id = id;
            this.name = name;
        }
        public int getId() {
            return id;
        }
        public String getName() {
            return name;
        }
        @Override
        public String toString() {
            return getClass().getName() + "[id=" + id + ",name=" + name + "]";
        }
    }
    public static Stream<Person> people() throws IOException {
        return Stream.of(new Person(1001, "Peter"), new Person(1002, "Paul"),
                new Person(1003, "Mary"));
    }
    public static void main(String[] args) throws IOException {
        Map<Integer, String> idToName = people().collect(
                Collectors.toMap(Person::getId, Person::getName));
        System.out.println("idToName: " + idToName);
        Map<Integer, Person> idToPerson = people().collect(
                Collectors.toMap(Person::getId, Function.identity()));
        System.out.println("idToPerson: " + idToPerson.getClass().getName() + idToPerson);
        idToPerson = people().collect(
                Collectors.toMap(Person::getId, Function.identity(),
                        (existingValue, newValue) -> { throw  new IllegalStateException(); }, TreeMap::new));
        System.out.println("idToPerson: " + idToPerson.getClass().getName() + idToPerson);
        Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
        Map<String, String> languageNames = locales.collect(
                Collectors.toMap(Locale::getDisplayLanguage,
                        l -> l.getDisplayLanguage(l),
                        (existingValue, newValue) -> existingValue));
        System.out.println("languageNames: " + languageNames);
        locales = Stream.of(Locale.getAvailableLocales());
        Map<String, Set<String>> countryLanguageSets = locales.collect(
                Collectors.toMap(
                        Locale::getDisplayCountry,
                        l -> Collections.singleton(l.getDisplayLanguage()),
                        (a, b) -> {
                            Set<String> union = new HashSet<>(a);
                            union.addAll(b);
                            return union;
                        }
                ));
        System.out.println("countryLanguageSets: " + countryLanguageSets);
    }
}

10.羣組和分區

groupingBy將具有相同特性的值羣聚成組。

Map<String, List<Locale>> countryToLocales = locales.collect(
        Collectors.groupingBy(Locale::getCountry));
List<Locale> swissLocales = countryToLocales.get("CN");
// [zh_CN]

每個Locale都有一個語言代碼(en)和一個國家代碼(US)。Locale en_US描述美國英語,而en_IE是愛爾蘭英語,某些國家有過個Locale。

當分類函數是斷言函數時,流的元素可以分區爲兩個列表:該函數返回true的元素和其他元素(partitioningBy比groupingBy更高效)。

Map<Boolean, List<Locale>> englishAndOtherLocales = locales.collect(
        Collectors.partitioningBy(l -> l.getLanguage().equals("en")));
List<Locale> englishLocales = englishAndOtherLocales.get(true);

使用groupingByConcurrent方法,就會使用並行流時獲得一個被並行組裝的並行映射表。

11.下游收集器

groupingBy方法會產生一個映射表,它的每個值都是一個列表,如果想處理這些列表,就需要提供一個下游收集器。
靜態導入java.util.stream.Collectors.*會使表達式更容易閱讀。

import static java.util.stream.Collectors.*;
Map<String, Set<Locale>> countryToLocales = locales.collect(
        groupingBy(Locale::getCountry, toSet()));

Java提供了多種可以將羣組元素約簡爲數字的收集器:
1.counting會產生收集到的元素的個數:

Map<String, Long> countryToLocaleCounts = locales.collect(
        groupingBy(Locale::getCountry, counting()));

2.summing(Int|Long|Double)會接收一個函數引元,將該函數應用到下游元素中,併產生它們的和:

Map<String, Integer> stateToCityPopulation = cities.collect(
        groupingBy(City::getState, summarizingInt(City::getPopulation)));

3.maxBy和minBy會接收一個比較器,併產生下游元素中的最大值和最小值:

Map<String, Optional<City>> stateToLargestCity = cities.collect(
        groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation))));

mapping方法會產生函數應用到下游結果上的收集器,並將函數值傳遞給另一個收集器:

Map<String, Optional<String>> stateToLongstCityName = cities.collect(
        groupingBy(City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length)))));

將城市羣組在一起,在每個州內部,生成各個城市的名字,並按照最大長度約簡。

9節中把語言收集到一個集中,使用mapping會有更加的解決方案:

Map<String, Set<String>> contryToLanguages = locales.collect(
        groupingBy(Locale::getDisplayCountry,
                mapping(Locale::getDisplayLanguage, toSet()))); 

9節中使用的是toMap而不是groupingBy,上述方式中,無需操心如何將各個集組合起來。

可以從每個組的彙總對象中獲取到這些函數值的總和、個數、平均值、最小值和最大值:

Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.collect(
                groupingBy(City::getState, summarizingInt(City::getPopulation)));
public class DownstreamCollectors {
    public static class City {
        private String name;
        private String state;
        private int population;
        public City(String name, String state, int population) {
            this.name = name;
            this.state = state;
            this.population = population;
        }
        public String getName() {
            return name;
        }
        public String getState() {
            return state;
        }
        public int getPopulation() {
            return population;
        }
    }
    public static Stream<City> readCities(String filename) throws IOException {
        return Files.lines(Paths.get(filename)).map(l -> l.split(", ")).
                map(a -> new City(a[0], a[1], Integer.parseInt(a[2])));
    }
    public static void main(String[] args) throws IOException {
        Stream<Locale> locales = Stream.of(Locale.getAvailableLocales());
        Map<String, Set<Locale>> countryToLocaleSet = locales.collect(groupingBy(
           Locale::getCountry, toSet()));
        System.out.println("countryToLocaleSet: " + countryToLocaleSet);
        locales = Stream.of(Locale.getAvailableLocales());
        Map<String, Long> countryToLocaleCounts = locales.collect(groupingBy(
           Locale::getCountry, counting()));
        System.out.println("countryToLocaleCounts: " + countryToLocaleCounts);
        Stream<City> cities = readCities("cities.txt");
        Map<String, Integer> stateToCityPopulation = cities.collect(
                groupingBy(City::getState, summingInt(City::getPopulation)));
        System.out.println("stateToCityPopulation: " + stateToCityPopulation);
        cities = readCities("cities.txt");
        Map<String, Optional<String>> stateToLongestCityName = cities.collect(groupingBy(
                City::getState, mapping(City::getName, maxBy(Comparator.comparing(String::length)))));
        System.out.println("stateToLongestCityName: " + stateToLongestCityName);
        locales = Stream.of(Locale.getAvailableLocales());
        Map<String, Set<String>> countryToLanguages = locales.collect(groupingBy(
                Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
        System.out.println("countryToLanguages: " + countryToLanguages);
        cities = readCities("cities.txt");
        Map<String, IntSummaryStatistics> stateToCityPopulationSummary = cities.collect(
                groupingBy(City::getState, summarizingInt(City::getPopulation)));
        System.out.println(stateToCityPopulationSummary.get("上海市"));
        cities = readCities("cities.txt");
        Map<String, String> stateToCityNames = cities.collect(
                groupingBy(City::getState, reducing("", City::getName, (s, t) -> s.length() == 0 ? t : s + ", " + t)));
        System.out.println("stateToCityNames: " + stateToCityNames);
        cities = readCities("cities.txt");
        stateToCityNames = cities.collect(groupingBy(City::getState, mapping(City::getName, joining(", "))));
        System.out.println("stateToCityNames: " + stateToCityNames);
    }
}

12.約簡操作

reduce方法是一種用於從流中計算某個值的通用機制。最簡單的形式將接受一個二元函數,並從前兩個元素開始持續應用它。如果該函數是求和函數:

List<Integer> values = ...;
Optional<Integer> sum = values.stream().reduce((x,y) -> x + y);

reduce會計算v0 + v1 + v2 +…,如果流爲空,方法會返回一個Optional。上面情況可以寫成reduce(Integer::sum)
通常v0 op v1 op v2 op…,調用函數op(vi, vi+1)寫作vi op vi+1。這項操作是可結合的:即組合元素時使用順序不應該成爲問題。(x op y) op z等於x op (y op z),這使得在使用並行流時,可以執行高效約簡。
求和,乘積,字符串連接,取最大值,最小值,求集的並與交等,都是可結合操作。減法不是一個可結合操作,(6-3)-2 ≠ 6 - (3 - 2)。
通常幺元值e使得e op x = x,可以使用這個元素做爲計算的起點:

Integer sum = values.stream().reduce(0, (x, y) -> x + y);
// 0 + v0 + v1 + v2 + ...

如果流爲空,則會返回幺元值。

如果有一個對象流,且想要對某些屬性求和,需要(T,T)->T這樣的函數,即引元和結果的類型相同的函數。
但如果類型不同,例如流的元素具有String類型,而累積結果是整數,首先需要一個累積器(total, word) -> total + word.length(),這個函數會被反覆調用產生累積的總和。但是當計算被並行化時,會有更多個這種類型的計算,需要提供第二個函數來將結果合併:

int result = words.reduce(0,
	(total, word) -> total + word.length(),
	(total1, total2) -> total1 + total2);

在實踐中reduce會顯得並不夠通用,通常映射爲數字流並使用其他方法來計算總和、最大值和最小值(words.mapToInt(String::length).sum(),因爲不涉及裝箱操作,所以更簡單也更高效)。

13.基本類型流

將整數收集到Stream< Integer >中,將每個整數都包裝到包裝器對象中是很低效的。流庫中具有專門的類型IntStream、LongStream和DoubleStream,用來直接存儲基本類型值,無需使用包裝器。short、char、byte和boolean,可以使用IntStream,對於float可以使用DoubleStream。

調用IntStream.of和Arrays.stream方法創建IntStream:

IntStream stream = IntStream.of(1, 1, 2, 3, 5);
stream = Arrays.stream(values, 2, 4); //values是一個數組

基本類型流還可以使用靜態的generate和iterate方法,此外,IntStream和LongStream有靜態方法range和rangeClosed,可以生成步長爲1的整數範圍:

IntStream zeroToNinetyNine = IntStream.range(0 ,100);
IntStream zeroToHundred = IntStream.rangeClosed(0 ,100);

CharSequence接口擁有codePoints和chars方法,可以生成有字符的Unicode碼或有UTF-16編碼機制的碼元構成的IntStream:

String sentence = "\uD835\uDD46 is the set of octonions.";
IntStream codes = sentence.codePoints();

對象流可以用mapToInt、mapToLong和mapToDouble將其轉換爲基本類型流:

Stream<String> words = ...;
IntStream lengths = words.mapToInt(String::length);

基本類型流轉換爲對象流,使用boxed方法:

Stream<Integer> integers = IntStream.range(0, 100).boxed();

基本類型流的方法與對象流的方法的主要差異:
1.toArray方法會返回基本類型數組。
2.返回結果OptionalInt|Long|Double,要用getAsInt|Long|double方法,而不是get方法
3.具有返回總和、平均值、最大值和最小值的sum、average、max和min方法,對象流沒有
4.summaryStatistics方法會產生一個類型爲Int|Long|DoubleSummaryStatistics的對象,他們可以同時報告流的總和、平均值、最大值和最小值。

Random類具有ints、longs和doubles方法,可以返回隨機數構成的基本類型流。

public class PrimitiveTypeStreams {
    public static void show(String title, IntStream stream) {
        final int SIZE = 10;
        int[] firstElements = stream.limit(SIZE + 1).toArray();
        System.out.print(title + ": ");
        for (int i = 0; i < firstElements.length; i++) {
            if (i > 0) {
                System.out.print(", ");
            }
            if (i < SIZE) {
                System.out.print(firstElements[i]);
            } else {
                System.out.print("...");
            }
        }
        System.out.println();
    }
    public static void main(String[] args) throws IOException {
        IntStream is1 = IntStream.generate(() -> (int)(Math.random() * 100));
        show("is1", is1);
        IntStream is2 = IntStream.range(5, 10);
        show("is2", is2);
        IntStream is3 = IntStream.rangeClosed(5, 10);
        show("is3", is3);
        Path path = Paths.get("alice30.txt");
        String contents = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
        Stream<String> words = Stream.of(contents.split("\\PL+"));
        IntStream is4 = words.mapToInt(String::length);
        show("is4", is4);
        String sentence = "\uD835\uDD46 is the set of octonions.";
        System.out.println(sentence);
        IntStream codes = sentence.codePoints();
        System.out.println(codes.mapToObj(c -> String.format("%X ", c)).collect(
                Collectors.joining()));
        Stream<Integer> integers = IntStream.range(0, 100).boxed();
        IntStream is5 = integers.mapToInt(Integer::intValue);
        show("is5", is5);
    }
}

14.並行流

流使得並行處理塊操作變得很容易,但必須有一個並行流。可以用Collection.parallelStream()方法從任何集合中獲取一個並行流:

Stream<String> parallelWords = words.parallelStream();

而parallel方法可以將任意的順序流轉換爲並行流:

Stream<String> parallelWords = Stream.of(strings).parallel();

只要在終結方法執行時,流處於並行模式,那麼所有的中間流操作都將被並行化。
當流操作並行運行時,其目標是要讓其返回結果與順序執行時返回的結果相同。重要的是,這些操作可以以任意順序執行。

對字符串流的所有短單詞計數:

String contents = new String(Files.readAllBytes(Paths.get("alice30.txt")), StandardCharsets.UTF_8);
List<String> wordList = Arrays.asList(contents.split("\\PL+"));
int[] shortWords = new int[12];
wordList.parallelStream().forEach(
        s -> { if (s.length() < 12) { shortWords[s.length()]++; }});
System.out.println(Arrays.toString(shortWords));

這是一種非常糟糕的代碼。傳遞給forEach的函數會在多個併發線程中運行,每個都會更新共享的數組。這是一種經典的競爭情況。如果多次運行這個程序,每次運行都會產生不同的計數值,而且每個都是錯的。

需要確保傳遞給並行流操作的任何函數都可以安全地並行執行,最佳方式是遠離易變狀態。如果用長度將字符串羣組,然後在分別計數,就可以安全地並行化計算:

Map<Integer, Long> shortWordCounts = wordList.parallelStream().
        filter(s -> s.length() < 10).collect(Collectors.groupingBy(String::length, Collectors.counting()));

默認情況下,有序集合(數組和列表或Stream.sorted)產生的流都是有序的。因此結果是完全可以預知的,運行相同操作兩次,結果是完全相同的結果。
排序並不排斥高效地並行處理。當計算stream.map()時,流可以被劃分爲n的部分,會並行處理。然後按照順序重新組裝起來。

當放棄排序需求時,可以被更有效地並行化。通過在流上調用unordered方法,就可以明確表示對排序不感興趣。在有序的流中,distinct會保留所有相同元素的第一個,這對並行化是一種阻礙,因爲處理每個部分的線程在其之前的所有部分都被處理完之前,並不知道應該丟棄哪些元素。
還可以通過放棄排序要求來提高limit方法的速度:

Stream<String> sample = words.parallelStream().unordered().limit(n);

合併映射表的代價很高昂,所以Collectors.groupByConcurrent方法使用了共享的併發映射表。爲了從並行化中獲益,映射表中值的順序不會與流中的順序相同。

Map<Integer, List<String>> result = words.parallelStream().collect(
        Collectors.groupingByConcurrent(String::length));

如果使用獨立於排序的下游收集器:

Map<Integer, Long> wordCounts = words.parallelStream().collect(
        Collectors.groupingByConcurrent(String::length, Collectors.counting()));

不要修改在執行某項流操作後會將元素返回到流中的集合(即使這樣修改是線程安全的)。流並不會收集它們的數據,數據總是在單獨的集合中。如果修改了這樣的集合,那麼流操作的結果就是未定義的(順序流和並行流都採用這種方式)。
因爲中間的流操作是惰性的,所以直到執行終結操作時纔對集合進行修改仍舊是可行的。儘管不推薦,但仍可以工作:

List<String> wordList = ...;
Stream<String> words = wordList.stream();
wordList.add("END");
long n = words.distinct().count();

但是下面是錯誤的:

Stream<String> words = wordList.stream();
words.forEach(s -> if(s.length() < 12) wordList.remove(s));

爲了讓並行流正常工作,需要滿足大量條件:
1.數據應該在內存中
2.流應該可以被高效地分成若干個子部分,由數組和平衡二叉樹支撐的流
3.流操作的工作量應該具有較大的規模,不要將所有流都轉化爲並行流,只有對已經位於內存中的數據執行大量計算操作時,才應該使用並行流
4.流操作不應該被阻塞

public class ParallelStreams {
    public static void main(String[] args) throws IOException {
        String contents = new String(Files.readAllBytes(Paths.get("alice30.txt")), StandardCharsets.UTF_8);
        List<String> wordList = Arrays.asList(contents.split("\\PL+"));
        // 代碼很糟糕
        int[] shortWords = new int[10];
        wordList.parallelStream().forEach(s -> {
            if (s.length() < 10) {
                shortWords[s.length()]++;
            }
        });
        System.out.println(Arrays.toString(shortWords));
        //再試一次結果可能會不同(也可能是錯誤的)
        Arrays.fill(shortWords, 0);
        wordList.parallelStream().forEach(s ->{
            if (s.length() < 10) {
                shortWords[s.length()]++;
            }
        });
        System.out.println(Arrays.toString(shortWords));
        // 補救措施:分組計數
        Map<Integer, Long> shortWordCounts = wordList.parallelStream()
                .filter(s -> s.length() < 10)
                .collect(groupingBy(String::length, counting()));
        System.out.println(shortWordCounts);
        Map<Integer, List<String>> result = wordList.parallelStream().collect(
                groupingByConcurrent(String::length));
        System.out.println(result.get(14));
        result = wordList.parallelStream().collect(
                groupingByConcurrent(String::length));
        System.out.println(result.get(14));
        Map<Integer, Long> wordCounts = wordList.parallelStream().collect(
                groupingByConcurrent(String::length, counting()));
        System.out.println(wordCounts);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章