Java SE 8 Lambda 標準庫概覽(下)

jopen 10年前發布 | 13K 次閱讀 Java8 Java開發

Java SE 8 Lambda 標準庫概覽

8,Collectors

    在目前的例子中,我們已經使用了collect()方法來收集流中的元素并放入List或Set中了。collec()方法的參數是一個收集器,它包含了收集或匯總多個元素到一個數據結構中的概念。Collectors 類提供了諸多常用收集器的工廠方法;toList()和toSet()是最常用的兩種,但是還有很多可以用來執行復雜轉換的收集器。

    收集器的輸入和輸出均為參數化的。toList() 的 收集器接受一個T類型的輸入并產生一個List<T>類型的輸出。一個稍微復雜一點的收集器是toMap,其中有多個版本。最簡單的版本接受一對函數,一個用來映射鍵,另一個用來映射值。它接受一個T作為輸入并產生一個Map<K, V>類型的輸出,其中K和V鍵值映射函數的結果類型。(更加復雜的版本允許我們自定義輸出結果的類型,或者解決多個元素映射同一個鍵時的重復)舉個例子,創建一個已知唯一鍵的反向索引,如catalog number:

Map<Integer, Album> albumsByCatalogNumber =
    albums.stream()
          .collect(Collectors.toMap(a -> a.getCatalogNumber(), a -> a));

    與toMap相關的是groupingBy。假如我們想要將最喜歡的歌曲以歌手分組制成表格。我們希望有一個接受歌曲(Track)為輸入,Map<Artist, List<Track>>為輸出的收集器。這切好與groupingBy的簡單形式的行為像匹配,它接受一個分類函數,并根據該函數生成一個映射鍵,該鍵的對應值是一組與鍵相關的輸入元素。

Map<Artist, List<Track>> favsByArtist =
    tracks.stream()
          .filter(t -> t.rating >= 4)
          .collect(Collectors.groupingBy(t -> t.artist));

    收集器可以被組合重用來產生更加復雜的收集器。groupingBy的簡單形式根據分類函數將元素組織到桶位中(這里是歌手),然后將所有映射到同一個桶位的元素放入List中。還有一個更加通用的版本允許我們使用另一個收集器來組織同一個桶位中的元素;這個版本接受一個分類函數,以及一個下游收集器作為參數,并且根據分類函數映射到同一個桶位的所有元素被收集到下游收集器中。(一個參數版本的groupingBy隱式的使用toLit()作為下游收集器)舉個例子,如果我們想要收集不同歌手的歌曲到一個Set中而不是List,我們可以使用toSet()收集器:

Map<Artist, Set<Track>> favsByArtist =
    tracks.stream()
          .filter(t -> t.rating >= 4)
          .collect(Collectors.groupingBy(t -> t.artist, 
                                         Collectors.toSet()));

    如果我們想要將歌曲以歌手和評價等級分類,來創建一個多層映射,我們可以這樣:

Map<Artist, Map<Integer, List<Track>>> byArtistAndRating =
    tracks.stream()
          .collect(groupingBy(t -> t.artist, 
                              groupingBy(t -> t.rating)));

    最后一個例子,假如我們想要創建一個歌曲標題中單次的頻率分布。我們首先使用Stream.flatMap() 和Pattern.splitAsStream()來將歌曲的名稱分割成不同的單詞,并產生一個所有歌曲名字中單詞的流。然后我們可以使用groupingBy作為分類函數,該函數以String.toUpperCase分類(這樣所有相同的單詞,忽略大小寫,都被認為是相同的,并放入同一桶位中),再使用count()收集器作為下游收集器來計算每個單詞出現的次數(沒有直接創建一個集合):

Pattern pattern = Pattern.compile(\\s+");
Map<String, Integer> wordFreq = 
    tracks.stream()
          .flatMap(t -> pattern.splitAsStream(t.name)) // Stream<String>
          .collect(groupingBy(s -> s.toUpperCase(),
                              counting()));

    flatMap()方法接受一個函數作為參數,該函數映射輸入元素(這里是歌曲)到某類流中(這里是歌曲名字的單詞)。它將映射函數應用到流中的每一個元素,然后用結果流中的內容替換每一個元素。(將這一過程分為兩個操作,首先將每一個元素映射到一個流中,然后將結果流中的所有內容整合到一個流中)那么這里,flatMap操作的結果是一個包含了所有歌曲的名稱單詞的流。然后我們將單詞分組到包含出現的單詞的桶位中,最后使用counting()收集器來計算每個桶位中單詞的個數。

    Collectors類有許多構造不同收集器的方法,這些收集器可以用于所有常見的查詢,統計,制表,并且你也可以實現自己的收集器。

 

9,并行的底層原理

    隨著Java SE 7中加入了Fork/Join框架,JDK中便有了高效實現的并行計算API。然而,使用Fork/Join的并行代碼與同等功能的串行代碼看起來非常不同(并且更大),這一直是并行化的一個障礙。通過在并行流和串行流上支持完全相同的操作,用戶可以移除這一障礙,不需要重寫代碼便可在串行和并行之間自由切換,這使得并行更加容易并且不易出錯。

    通過遞歸分解實現一個并行計算所涉及的步驟包括:將一個問題分為一些子問題,解決子問題并產生部分結果,然后將這些部分結果合并。Fork/Join機制被設計為自動化這一過程。

    為了支持在任意流數據源上的多有操作,我們將流數據源抽象為一個叫做Spliterator的模型,這是傳統Iterator的一個廣義化。為了支持順序訪問數據元素,Spliterator還支持分解:就像一個Iterator允許你切除單個元素并保持剩余元素依舊為Iterator,Spliterator允許你在輸入元素上分割一個更大的數據塊生成一個新的Spliterator,并切保持剩余的數據依舊是最初的Spliterator。(兩個Spliterator依舊可以再分割)另外,Spliterator可以提供數據源的元數據像是元素個數(如果知道)以及一些列boolean值(像是‘元素是有序的’),這些用來優化Stream框架。

    這一方法將遞歸分解的結構性質與在可分解的數據結構上進行并行計算的算法分離開來。數據結構的作者只需提供一個分界邏輯,然后就可以直接在流操作的并行計算上獲益了。

    大部分用戶不需要實現Spliterator;他們只需要在集合上使用stream()方法即可。但是如果你曾經實現一個集合或其他流數據源,那么可能你想自定義一個Spliterator。Spliterator的API如下:

public interface Spliterator<T> {
    // Element access
    boolean tryAdvance(Consumer<? super T> action);
    void forEachRemaining(Consumer<? super T> action); 

    // Decomposition
    Spliterator<T> trySplit();

    // Optional metadata
    long estimateSize();
    int characteristics();
    Comparator<? super T> getComparator();
}

    基本接口像Iterable和Collection提供了正確但低效的spliterator實現,但是子接口(如Set)和一些具體實現(如ArrayList)利用父接口不可用的信息實現了更加高效的spliterator。spliterator實現的質量將會影響流的執行性能;從split方法分割出均衡的結果會提高CPU的利用率,并且提供正確的元數據還會有其他優化。

 

10,出現順序

    許多數據源如列表,數組,以及I/O通道,都有一個自然的出現順序,這意味著元素出現的順序是有意義的。其他的數據源,像是HashSet則沒有定義出現順序(因此HashSet的Iterator允許以任意順序提供元素)

    Spliterator中維護的一個boolean屬性,并用于流實現的就是當前流是否是定義出現順序的。除了一些例外(如Stream.forEach()和Stream.findAny()),并行操作是被約束為需要出現順序的。這意味著像下面的流管道:

List<String> names = people.parallelStream()
                           .map(Person::getName)
                           .collect(toList());

    name必須與數據源中相應people保持相同順序。通常這是我們想要的,并且對于許多流操作,這不需要額外的消耗。另一方面,如果數據源是HashSet,name可能會出現任意順序,甚至并行操作下出現不同順序。

 

11,JDK中的流和Lambda

    將Stream暴露為一個頂級抽象模型,我們想要確保流的特性能盡可能的貫穿整個JDK,Collection已經被增強為通過stream()和parrallelStream()可以轉換為流;數組可以通過Arrays.stream()轉換為流。

    另外Stream中有一些靜態工廠方法(以及相關的基本類型具體實現)用來創建流,像是Stream.of,Stream.generate,和IntStream.range。許多其他類也有了一些流轉換方法,像是String.chars,BufferedReader.lines,Pattern.splitAsStream,Random.ints和BitSet.stream。

最后,我們提供一些列被庫作者使用的API來構造流,他們希望暴露的非標準的流聚合功能。用來創建流的最小的信息是一個Iterator,但是如果創建者有額外的元數據(如大小),庫可以通過實現Spliterator來提供更加高效的流實現。

 

12,比較器工廠

    Comparator 類中添加了許多新的用來構建比較器的方法。

    靜態方法Comparator.comparing()接受一個提取Comparable排序鍵的函數并生成一個Comparator。它的實現非常簡單:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor) {
    return (c1, c2) 
        -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

    這樣的方法是高階函數的一個例子---接受一個函數作為參數或者生成一個新的函數的函數。但是這種方法真正強大之處是它的更好的可組合性。舉個例子,Comparator有一個默認的方法來翻轉比較方向。那么,為了將people別表以lastname 反序排序,我們只需簡單的創建一個之前的比較器,然后要求它翻轉自己即可:

people.sort(comparing(p -> p.getLastName()).reversed());

    同樣的,默認方法thenComparing允許我們接受一個Comparator并且改善它的行為當最初的比較器視兩個元素相同時。以lastname和firtname排序people列表,我們可以這樣:

Comparator<Person> c = Comparator.comparing(p -> p.getLastName())
                                 .thenComparing(p -> p.getFirstName());
people.sort(c);

 

13,可變集合操作

    集合上的流操作生成一個新的值,集合,或者副作用。然而,有時我們就想改變原始集合,并且Collection,List,Map中已經添加了一些新方法來利用Lambda表達式,像是Iterable.forEach(Consumer),Collection.removeAll(Predicate),List.replaceAll(UnaryOperator),List.sort(Comparator),以及Map.computeIfAbsent()。另外,ConcurrentMap中的非原子版本的方法,如replace和putIfAbsent已經從Map中去除了。

 

14,總結

    向語言中添加Lambda表達式是一個巨大的進步,開發者每天都在使用核心庫來完整他們的工作,因此語言的煙花需要配合核心庫的演化以便用戶可以盡快使用新特性。新標準庫的中心特性就是Stream的概念,這為我們提供了強大的基于數據集的聚合操作工具,并且已經深度整合到當前的集合類以及其他JDK類中來了。

Java SE 8 Lambda 標準庫概覽(上)

來自:http://my.oschina.net/HeliosFly/blog/194605

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!