Lambda說明:類庫的修改

原文地址: http://cr.openjdk.java.net/~briangoetz/lambda/lambda-libraries-final.html

這是對OpenJDK Lambda(http://openjdk.java.net/projects/lambda/).項目 JSR 335 主要類庫增強的非正式概述趁耗。在閱讀這篇文章之前,我們建議你首先了解Java8的新特性,具體內(nèi)容可以在State of the Lambda中找到,

背景

如果Lambda表達式最初就存在于Java中,類似Collections這樣的API就會與現(xiàn)在截然不同。由于JSR 335會將Lambda表達式增加到Java中,這讓Collections這樣的接口顯得更加過時攘乒!雖然從頭開始構(gòu)建一個全新的集合框架(Collections Framework)這個想法十分具有誘惑力,但是集合類接口貫穿于整個Java生態(tài)系統(tǒng),想要完全替換掉它們可能需要很長時間豺旬。因此我們采用了循序漸進的策略,為現(xiàn)有的接口(比如Collection, List和Iterable)增加了拓展方法,添加了一個流(比如 java.util.stream.Stream)的抽象 (stream abstraction)用于數(shù)據(jù)集的聚合操作(aggregate operations)疑故,改進現(xiàn)有的類來提供流視圖(stream views),引入新的語法可以讓人們不通過ArrayLists和HashMaps類來進行相應(yīng)操作揽惹。(這并不是說Collections這樣的輔助類永遠不會被替代,很顯然除了設(shè)計不符合Lambda以外,它還有更多其他的限制旭斥。一個更合適的集合類框架可能需要考慮到JDK未來版本的變化和趨勢)

這個項目的一個核心目的是使并行化編程更加容易悬包。雖然Java 已經(jīng)提供了對并發(fā)和并行的強大支持,但是開發(fā)者仍然在需要將串行代碼遷移至并發(fā)時面對著不必要的障礙谊却。因此,我們提倡一種無論在串行還是并行下都十分友好的語法和編程習(xí)慣柔昼。我們通過將關(guān)注點從"怎么進行代碼計算"轉(zhuǎn)移到"我們要計算什么"達到這目的。而且我們要在并行的易用性和可見性中找到一個平衡點,達到一個清晰(explicit )但是不突兀(unobstrusive)的并行化是我們的最終目標炎辨。(使并行對用戶完全透明會導(dǎo)致很多不確定性,也會帶來用戶意想不到的數(shù)據(jù)競爭)

內(nèi)部 vs 外部迭代(iteration)

Collections框架依賴于外部迭代的概念,提供通過實現(xiàn)Iterable接口列舉出它的元素的方法,用戶使用這個方法順序遍歷集合中的元素捕透。例如,如果我們有一個形狀(shape)的集合類,然后想把里面每一個形狀都涂成紅色,我們會這么寫:

for (Shape s : shapes) {
    s.setColor(RED);
}

這個例子闡述了什么是外部迭代,這個for-each循環(huán)直接調(diào)用shapes的iterator方法,依次遍歷集合中元素。外部遍歷非常直接了當(dāng),不過也有一些問題:
1) Java的for循環(huán)本身是連續(xù)的,必須按照集合定義的順序進行操作
2) 它剝奪了類庫對流程控制的機會,我們本有可能通過重排序,并行化,短路操作(short-circuiting)和惰性求值(laziness)來獲得更好的性能碴萧。
注:惰性求值可以參考 https://hackhands.com/lazy-evaluation-works-haskell/

有時候,我們希望利用for循環(huán)帶來的好處(連續(xù)并且有序),但是大部分情況下它妨礙了性能的提升乙嘀。

另一種替代方案是內(nèi)部迭代,它并不控制迭代本身,客戶端將控制流程委托給類庫,將代碼分片在不同的內(nèi)核進行計算。

和上面對應(yīng)的內(nèi)部迭代的例子如下:

shapes.forEach(s -> s.setColor(RED));

從語法上看差別似乎并不大,實際上他們有著巨大的差異破喻。操作的控制權(quán)從客戶端轉(zhuǎn)移到了類庫之中,不但可以抽象出通用的控制流程操作,還可以使用惰性求值,并行化和無序執(zhí)行來提高性能(無論這個forEach的實現(xiàn)是否利用了這些特性,至少決定權(quán)在實現(xiàn)本身虎谢。內(nèi)部迭代提供了這種可能性,但是外部迭代不可能做到這一點)。

外部迭代將"什么"(將形狀涂成紅色)和"怎么做"(拿到迭代器來順序迭代)混在一起曹质。內(nèi)部迭代使客戶端決定"什么",讓類庫來控制"怎么做"婴噩。這樣有幾個潛在的好處:

  1. 客戶端代碼可以更加清晰,因為只需要關(guān)注解決問題本身,而不是通過什么形式來解決問題。
  2. 我們可以把復(fù)雜的代碼優(yōu)化移至類庫中,所有用戶都可從中受益羽德。

流 (Streams)

我們在Java8中引入了一個新的關(guān)鍵的類庫"stream", 定義在java.util.stream包中几莽。(我們有不同的Stream類型, Stream<T> 代表了引用類型是object的流,還有一些定制化的流比如IntStream來描述原始類型的流) 流代表了值的序列,并且暴露(expose)了一系列的聚合操作,允許我們很輕松并且清晰的對值進行通用的操作。對于獲取集合,數(shù)組以及其他數(shù)據(jù)源的流視圖(stream view),類庫提供了非常便捷的方式宅静。

流操作被鏈接在一起至"管道"(pipeline)中章蚣。例如,如果我們只想把藍色的形狀涂成紅色,我們可以這樣:

shapes.stream() 
      .filter(s -> s.getColor() == BLUE)
      .forEach(s -> s.setColor(RED));

Collection的stream方法產(chǎn)生了一個集合所有元素的流視圖,filter操作接著產(chǎn)生了一個只含有藍色形狀的流,我們再通過forEach方法將其涂成紅色。

如果我們想把藍色的形狀收集到一個新的List當(dāng)中,我們可以這樣:

List<Shape> blue = shapes.stream()
                         .filter(s -> s.getColor() == BLUE)
                         .collect(Collectors.toList());

collect操作將輸入的元素收集到一個聚合體(aggregate, 比如List)或者一個總結(jié)概述(summary description)中姨夹。collection中的參數(shù)表示應(yīng)當(dāng)如何進行聚合究驴。在這里,我們用了了toList,這只是一個簡單的把元素聚合到一個List里的方法(更多細節(jié)請參照“Collectors”章節(jié))。

如果每個形狀都在一個Box里面,并且我們想知道哪些Box至少包含一個藍色的形狀,我們可以這樣:

Set<Box> hasBlueShape = shapes.stream()
                              .filter(s -> s.getColor() == BLUE)
                              .map(s -> s.getContainingBox())
                              .collect(Collectors.toSet());

map操作產(chǎn)生了一個流,這個流的值由輸入元素的映射(這里返回的是包含藍色形狀的Box)產(chǎn)生匀伏。

如果我們想計算出藍色形狀的總重量,我們可以這樣:

int sum = shapes.stream()
                .filter(s -> s.getColor() == BLUE)
                .mapToInt(s -> s.getWeight())
                .sum();

至此,我們還沒有提供以上Stream操作的具體簽名的詳細信息; 這些例子僅僅是為了闡述設(shè)計Streams框架想要解決的問題。

流(Streams) vs集合(Collections)

流和集合盡管具有表面上的相似之處,但是他們設(shè)計的目標完全不同蝴韭。集合主要關(guān)注在有效的管理和訪問它的元素够颠。與之相反,流并不提供直接訪問或者操作它的元素的方法,而是關(guān)注對執(zhí)行在聚合數(shù)據(jù)源的計算操作的聲明式描述(declaratively describing)。

因此,流和集合主要有以下幾點不同:
1) 沒有存儲榄鉴。流不存在值的存儲,而是通過有著一系列計算步驟的管道來承載數(shù)據(jù)源(可以是數(shù)據(jù)結(jié)構(gòu),可以是生成的函數(shù),可以是I/O通道等等)中的值
2) 函數(shù)式本質(zhì)履磨。對于流的操作產(chǎn)生的結(jié)果并不改變它基本的數(shù)據(jù)源蛉抓。
3) 惰性傾向。很多流操作(例如過濾,映射,排序或者去重)都可以惰性實現(xiàn)剃诅。這一點有助于整個管道的single-pass執(zhí)行,也有助于高效的實現(xiàn)短路操作
4) 邊界不限定巷送。很多問題我們可以轉(zhuǎn)換為無限流(infinite stream)的形式,用戶可以一直使用流中的數(shù)據(jù),直到滿意為止(比如完全數(shù)問題就可以輕易的轉(zhuǎn)換為對所有整數(shù)的過濾操作),而集合類則是有限的。(如果需要在有限的時間內(nèi)終止一個無限流,我們可以使用短路操作,或者可以在流中直接調(diào)用一個迭代器進行手動遍歷)

作為一個API, 流和集合類之間完全獨立矛辕。因此我們可以很輕易地把一個集合作為流的數(shù)據(jù)源(集合有stream和parallelStream 方法)或者把流中的數(shù)據(jù)轉(zhuǎn)儲(dump)到一個結(jié)合中(使用collect操作),Collection以外的聚合體也可以作為流中的數(shù)據(jù)源笑跛。很多JDK中的類,例如BufferedReader, Random, 和 BitSet已經(jīng)被改進,也可以作為流的數(shù)據(jù)源。Arrays的stream方法提供了一個數(shù)組的流視圖聊品。事實上,任何可以用Iterator描述的類都可以作為流的數(shù)據(jù)源飞蹂。如果提供了更多的信息(例如大小或者排序信息),類庫可以提供優(yōu)化的執(zhí)行。

惰性求值(Laziness)

類似filter或者mapping這種的操作可以是"急性"(在filter方法返回之前對所有元素進行filter)或者"惰性"(只去按需過濾數(shù)據(jù)源中的元素)的翻屈。惰性計算可以給我們帶來潛在收益,比如我們可以將filter和管道中的其他操作融合,以免進行多次數(shù)據(jù)傳遞陈哑。與此類似,如果我們在一個大的數(shù)據(jù)集合中根據(jù)某些條件尋找第一個元素,我們可以在找到以后立刻停止而不是處理整個數(shù)據(jù)集(對于有界的數(shù)據(jù),源惰性求值僅僅是一個優(yōu)化措施。但是它使對無界數(shù)據(jù)源的操作成為了可能,而如果采用"急性"的方式,那我們永遠停不下來)伸眶。

無論采用怎樣的實現(xiàn)方式,像filter或者mapping這樣的操作可以被認為是"天然的惰性"惊窖。另一方面,求值運算如sum, "副作用運算"(side-effect-producing)如forEach是"天然的急性",因為他們必須生成一個具體的值。

在如下的一個管道中 :

int sum = shapes.stream()
                .filter(s -> s.getColor() == BLUE)
                .mapToInt(s -> s.getWeight())
                .sum();

filter和mapping操作是惰性的厘贼。這意味著直到開始sum操作時我們才會從數(shù)據(jù)源中取值界酒。并且執(zhí)行sum操作時我們會把filter,mapping合并使數(shù)據(jù)只被傳遞一次。這使得我們減少了管理中間變量所需的記賬(bookkeeping)消耗涂臣。

很多循環(huán)可以被重新描述為從數(shù)據(jù)源獲取數(shù)據(jù)的聚合操作,先進行一系列的惰性操作(filter, mapping...)然后再執(zhí)行一個急性操作(forEach, toArray,collect...),比如 filter-map-accumulate或者filter-map-sort-foreach盾计。天然惰性的操作適合用于計算臨時中間結(jié)果,我們在API設(shè)計的時候利用了這個特點,filter和map返回了一個新的stream而不是一個collection。
在Stream API中, 返回一個stream的操作是惰性的,返回一個非stream或者沒有返回值的操作是急性的赁遗。大多數(shù)情況下,潛在的惰性操作應(yīng)用于聚合上,這也是我們希望看到的 -- 每個階段都會獲取一個輸入流,對其進行一些轉(zhuǎn)換,然后將值傳遞到管道的下一階段署辉。

在source-lazy-lazy-eager 這種管道中,惰性大多不可見,因為計算過程夾在source和生成結(jié)果的操作之間。在規(guī)模相對小的API中會有很好的可用性和不錯的性能岩四。

一些急性方法,例如anyMatch(Predicate)或者findFirst同樣也可以用來進行短路操作,只要他們能確定最終結(jié)果,執(zhí)行就可以被結(jié)束哭尝。例如我們有以下管道

Optional<Shape> firstBlue = shapes.stream()
                                  .filter(s -> s.getColor() == BLUE)
                                  .findFirst();

因為filter這一步是惰性的,因此findFirst只有在獲得一個元素以后才會把它從上游取出。這意味著我們只需要在輸入(filter)應(yīng)用predicate直至找到一個predicate的結(jié)果是true的元素,而不需要在所有的元素應(yīng)用predicate剖煌。findFirst方法返回了一個Optional,因為有可能所有元素都不滿足條件材鹦。Optional描述了一個可能存在的值。

用戶其實無需在意惰性,類庫已經(jīng)做好了必要的事情來精簡運算耕姊。

并行化

管道流可以選擇串行或并行執(zhí)行,除非顯式調(diào)用并行流,JDK默認實現(xiàn)返回一個串行流(串行流可以通過parallel方法轉(zhuǎn)化為并行流)桶唐。

之前重量累加的方法可以直接通過調(diào)用parallelStream方法使其變成并行流。

int sum = shapes.parallelStream()
                .filter(s -> s.getColor() == BLUE)
                .mapToInt(s -> s.getWeight())
                .sum();

雖然對于同樣的計算,串行和并行看起來十分類似,但是并行流明確表示了它是并行的(我們并不需要像以前那樣為并行而寫一大堆代碼)茉兰。

stream的數(shù)據(jù)源可能是可變(mutable)集合,遍歷的過程中數(shù)據(jù)源被修改的可能性是存在的尤泽。流則期望在操作過程中,數(shù)據(jù)源能保持不變。如果數(shù)據(jù)源只被一個線程使用,我們只需保證輸入的Lambda不會更改數(shù)據(jù)源(這和外部迭代的限定是一樣的,大部分會拋出ConcurrentModificationException)。我們將這個要求稱為不可干擾坯约。

最好避免傳入stream中的Lambda表達式帶來任何"副作用"(side-effects)熊咽。雖然一些副作用比如打印一些值進行調(diào)試通常是線程安全的,但是從Lambda中獲取可變(mutable)變量可能會引起數(shù)據(jù)競爭(data racing)。這是因為一個Lambda有可能在多個線程內(nèi)被執(zhí)行,對于數(shù)據(jù)的執(zhí)行順序并不一定是他們看起來的順序闹丐。不可干擾不僅針對數(shù)據(jù)源,同樣也指 不能干擾其它的Lambda,例如在一個Lambda對一個可變數(shù)據(jù)源進行修改的時候,另外一個Lambda需要讀取它横殴。

只要不可干擾這個條件滿足,即使對非線程安全的數(shù)據(jù)源(比如ArrayList),我們也可以安全的進行并行操作。

舉例說明

以下是JDK中 Class這個類 getEnclosingMethod 方法的一部分,它遍歷了所有聲明的(declared)方法,匹配方法名,返回方法類型,方法個數(shù)和參數(shù)類型卿拴。

for (Method m : enclosingInfo.getEnclosingClass().getDeclaredMethods()) {
     if (m.getName().equals(enclosingInfo.getName()) ) {
         Class<?>[] candidateParamClasses = m.getParameterTypes();
         if (candidateParamClasses.length == parameterClasses.length) {
             boolean matches = true;
             for(int i = 0; i < candidateParamClasses.length; i++) {
                 if (!candidateParamClasses[i].equals(parameterClasses[i])) {
                     matches = false;
                     break;
                 }
             }
             if (matches) { // finally, check return type
                 if (m.getReturnType().equals(returnType) )
                     return m;
             }
         }
     }
 }

 throw new InternalError("Enclosing method not found");

如果使用stream,我們可以消除臨時變量并且把控制流程置于類庫中衫仑。我們通過反射獲得方法列表,通過Arrays.stream把它轉(zhuǎn)換為一個Stream,然后使用一系列filter過濾掉名字,參數(shù)類型和返回類型不匹配的方法。findFirst這個方法的返回值是一個Optional,我們可以獲取并返回或者拋出異常巍棱。

return Arrays.stream(enclosingInfo.getEnclosingClass().getDeclaredMethods())
             .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
             .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
             .filter(m -> Objects.equals(m.getReturnType(), returnType))
             .findFirst()
             .orElseThrow(() -> new InternalError("Enclosing method not found");

這個版本的代碼更為緊湊,可讀性強而且不容易出錯惑畴。

流操作對于集合的臨時查詢(ad hoc queries)十分有效。假設(shè)我們有一個"音樂庫"的應(yīng)用,其中有一系列的專輯,專輯又有它的名字和一系列歌曲,每首歌曲又有它的名字,作者和評分航徙。

假設(shè)我們需要找到所有評價在4分以上的歌曲所在的專輯,并且按專輯名字排序如贷。我們可以這樣:

List<Album> favs = new ArrayList<>();
for (Album a : albums) {
    boolean hasFavorite = false;
    for (Track t : a.tracks) {
        if (t.rating >= 4) {
            hasFavorite = true;
            break;
        }
    }
    if (hasFavorite)
        favs.add(a);
}
Collections.sort(favs, new Comparator<Album>() {
                           public int compare(Album a1, Album a2) {
                               return a1.name.compareTo(a2.name);
                           }});

如果使用流操作,我們只需要3個主要步驟:

  1. 在專輯中是否存在評價在4星以上的歌曲
  2. 對專輯進行排序
  3. 將滿足條件的專輯放到一個列表中
List<Album> sortedFavs =
  albums.stream()
        .filter(a -> a.tracks.anyMatch(t -> (t.rating >= 4)))
        .sorted(Comparator.comparing(a -> a.name))
        .collect(Collectors.toList());

Comparator.comparing 方法利用了一個Lambda返回的可比較的key的方法,返回一個比較器來做比較 (詳細內(nèi)容請參照"比較器工廠"章節(jié))

收集器(Collectors)

目前為止出現(xiàn)的例子中,我們使用collect方法,傳入一個Collector參數(shù),把stream中的元素收集至一個List或者Set這樣的數(shù)據(jù)結(jié)構(gòu)中。Collectors這個類包含了很多通用collector的工廠方法,toList和toSet是最常用的兩種,此外還有很多更復(fù)雜的對數(shù)據(jù)轉(zhuǎn)換的方法到踏。

收集器通過輸入和輸出類型進行參數(shù)化杠袱。toList的輸入類型是T,輸出類型是List<T>。稍微復(fù)雜一點的Collector是toMap,有幾個不同的版本窝稿。最簡單的版本是利用一對(pair)函數(shù),一個把輸入映射為map中的key,另外一個把其映射為value楣富。輸入?yún)?shù)是一個T,最后生成map<K,V>, K和V分別是之前提到的映射函數(shù)產(chǎn)生的結(jié)果(更復(fù)雜的版本允許自定義生成結(jié)果的類型,或者解決映射過程中出現(xiàn)重復(fù)的key的情況)。例如,有一組有唯一的key(CatalogNumber)的數(shù)據(jù)伴榔,我們需要根據(jù)他生成反向索引:

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

跟map相關(guān)的是groupingBy纹蝴。假設(shè)我們想根據(jù)作者來列出我們喜歡的曲目,我們想要一個Collector, 歌曲(Track)是入?yún)?生成一個Map<Artist,List<Track>>,這個需求和最簡單的具有g(shù)roupingBy的collector恰好匹配,這個collector利用一個分類函數(shù)(classification function)生成一個map,它的值是一個對應(yīng)生成的key的List。

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

Collectors可以組合和重用產(chǎn)生復(fù)雜的收集器踪少。最簡單的groupingBy收集器根據(jù)分類函數(shù)將元素分組并放入桶(bucket)中,然后再把映射到同一個桶中的元素放入一個List里面塘安。對于使用收集器來組織桶中的元素,我們有一個更通用的版本。我們將分類函數(shù)和下游收集器作為參數(shù),依據(jù)分類函數(shù)分到同一個桶的所有元素都會傳遞給下游收集器援奢。(一個參數(shù)的groupingBy方法隱式使用toList方法作為下游收集器)兼犯。例如我們?nèi)绻氚衙總€作者相關(guān)的歌曲收集到Set而不是List中,我們可以使用toSet:

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

如果我們想根據(jù)評分和作者創(chuàng)建一個多層的map,我們可以這樣:

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

最后一個例子,假設(shè)我們想得到在曲目標題中單詞的出現(xiàn)頻率的分布。首先可以使用Stream.flatMap和Pattern.splitAsStream拿到曲目的流,把曲目的名字分解成單詞,再生成一個單詞的流集漾。然后可以使用groupingBy函數(shù),傳入String.toUpperCase作為分類函數(shù)(這里我們忽略單詞的大小寫)并且使用counting收集器作為下游收集器來統(tǒng)計每個單詞的出現(xiàn)頻率(這樣我們不需要創(chuàng)建中間集合):

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方法將一個把輸入元素映射到流中的函數(shù)作為參數(shù),它將這個函數(shù)應(yīng)用到每個輸入元素中,使用生成的流的內(nèi)容替換每個元素(這里我們認為有兩個操作,首先將流中的每個元素映射到零個或者多個其他元素的流中, 然后把所有的結(jié)果扁平化到一個流當(dāng)中)切黔。因此flapMap的結(jié)果是一個包含所有曲目中不同單詞的流。然后把單詞進行分組放入桶中,再用counting收集器來獲得桶中單詞出現(xiàn)的次數(shù)具篇。

Collector這個類有很多方法構(gòu)建collector,可用于常見的查詢,匯總和列表,你也可以實現(xiàn)你自己的Collector纬霞。

隱藏的并行(Parallelism under the hood)

Java7中新增了Fork/Join框架,提供了一個高效并行計算的API。然而Fork/Join框架看起來與等效的串行代碼完全不同,這妨礙了并行化的實現(xiàn)驱显。串行和并行流的操作完全一樣,用戶可以輕松在串行/并行之間切換而不需要重寫代碼,這使得并行化更容易實施而且不易出錯诗芜。

通過遞歸分解實現(xiàn)并行計算的步驟是:將問題分解為子問題,順序解決并產(chǎn)生部分結(jié)果,然后將兩個部分結(jié)果組合侨舆。Fork/Join框架用來設(shè)計自動完成以上過程。

為了支持在任何數(shù)據(jù)源的流上的全部操作,我們使用一個稱為Spliterator的抽象方式將流的數(shù)據(jù)源模塊化绢陌,它是傳統(tǒng)迭代器的泛化(generalization)。除了支持對數(shù)據(jù)元素的順序訪問以外,Spliterator還支持分解(decomposition)功能:類似于迭代器可以剝離單個元素并保留其余元素,Spliterator可以剝離一個更大的塊(通常是一半)把它放入一個新的Spliterator中,把剩下的元素保留在原來的Spliterator中(這兩個Spliterator還可以進行進一步的分解)熔恢。此外,Spliterator可以提供數(shù)據(jù)源的元數(shù)據(jù)比如元素的數(shù)量或者一組boolean(比如元素是否被排序), Stream框架可以通過這些元數(shù)據(jù)進行優(yōu)化執(zhí)行脐湾。

這種方式把遞歸分解的結(jié)構(gòu)特性和算法分離,而且對于可分解的數(shù)據(jù)可以并行執(zhí)行。數(shù)據(jù)結(jié)構(gòu)的作者只需要提供數(shù)據(jù)分解的邏輯,就可以立即在stream上并行執(zhí)行提高效率叙淌。

大多數(shù)用戶不需要實現(xiàn)Spliterator, 只需要在現(xiàn)有的集合上使用stream等方法秤掌。但是如果你需要實現(xiàn)一個集合類或者其他stream的數(shù)據(jù)源,你可能需要自定義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();
}

基礎(chǔ)接口比如Iterable和Collection提供了正確但是低效的spliterator實現(xiàn),但是子接口(比如Set)或者實現(xiàn)類(比如ArrayList) 利用基礎(chǔ)接口無法獲得的一些信息復(fù)寫了spliterator,使其更加高效鹰霍。spliterator實現(xiàn)的質(zhì)量會影響stream執(zhí)行的效率,返回一個比較均衡分割結(jié)果的split方法會提高CPU利用率,如果能提供正確的元數(shù)據(jù)也會對優(yōu)化提供幫助闻鉴。

出現(xiàn)順序(Encounter Order)

很多數(shù)據(jù)源例如lists,arrays和I/O channel有其自帶的出現(xiàn)順序,這意味著元素出現(xiàn)的順序很重要茂洒。其他例如HashSet沒有定義出現(xiàn)順序(因此HashSet的迭代器可以處理任意順序的元素)

由Spliterator紀錄并且應(yīng)用在stream的實現(xiàn)中的特征之一便是stream是否定義了出現(xiàn)順序孟岛。除了幾個特例(比如Stream.forEach 和Stream.findAny),并行操作受到出現(xiàn)順序的限制,這意味著在以下stream管道中:

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

結(jié)果中names的順序必須和輸入流中的順序一致督勺。通常情況下,這是我們想要的結(jié)果,而且對很多流操作而言,存儲這個順序代價并不大渠羞。如果數(shù)據(jù)源是一個HashSet,那么結(jié)果中的names可以以任意順序出現(xiàn),而且在不同的執(zhí)行中的順序也會不一樣。

JDK中的流和Lambda

我們希望通過把Stream的抽象級別提高使得它的特性盡可能廣泛應(yīng)用于JDK中智哀。Collection已經(jīng)增加了stream和parallelStream方法來把集合轉(zhuǎn)換成流,數(shù)組可以使用Arrays.stream方法進行轉(zhuǎn)換次询。

此外,Stream中有靜態(tài)工廠方法來創(chuàng)建流,比如Stream.of, Stream.generate和IntStream.range。還有很多其他的類也增加了Stream相關(guān)的方法,比如BufferedReader.lines, Pattern.splitAsStream, Random.ints, 和 BitSet.stream瓷叫。

最后,我們提供了一組構(gòu)建流的API給希望在非標準聚合(non-standard aggregates)上使用stream功能的類庫作者屯吊。創(chuàng)建流所需的最小"信息"是一個迭代器,如果可以額外提供元數(shù)據(jù)(比如size),JDK在實現(xiàn)Spliterator的時候會更有效率(就像現(xiàn)有的集合類那樣)

比較器工廠(Comparator factories)

Comparator 這個類已經(jīng)添加了一些對于構(gòu)建比較器十分有用的新方法。

Comparator.comparing這個靜態(tài)方法利用了一個提取可比較(Comparable)key并且生成一個比較器的方法,實現(xiàn)如下:

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));
}

以上方法是一個"高階函數(shù)"(higher order functions)的例子 --- 高階函數(shù)指至少滿足一下一個條件的函數(shù):
1) 接收一個或者多個函數(shù)作為輸入
2) 輸出一個函數(shù)
利用這個comparing我們可以減少重復(fù),簡化客戶端代碼,例子如下:

List<Person> people = ...
people.sort(comparing(p -> p.getLastName()));

這個比老方法清晰很多,通常包含了一個實現(xiàn)了Comparator的匿名內(nèi)部類實例摹菠。但是這種方法真正牛逼的地方在于提高了"組合性"盒卸。比如Comparator有一個默認方法來顛倒順序,所以我們?nèi)绻胍孕盏哪嫘蜻M行排列,我們可以創(chuàng)建和之前一樣的comparator,然后讓它進行逆序:

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

類似的是,當(dāng)初始比較器認為兩個元素一樣的時候,thenComparing這個默認方法允許你獲得比較器并且改進它的行為。如果要我們根據(jù)名+姓排序的話,我們可以這樣:

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

可變集合操作(Mutative collection operations)

集合的Stream操作產(chǎn)生了一個新值,集合或者副作用辨嗽。然而有時我們想對集合進行直接修改,我們在Collection,List和Map中引入了一些新方法來利用Lambda達到目的世落。比如terable.forEach(Consumer), Collection.removeAll(Predicate), List.replaceAll(UnaryOperator), List.sort(Comparator), 和 Map.computeIfAbsent()。此外,我們也把ConcurrentMap中的一些方法例如replace和putIfAbsent增加了非原子操作的版本放進了Map中糟需。

總結(jié)

雖然引入Lambda是一個巨大的進度,但是開發(fā)者依舊每天使用核心庫完成工作屉佳。所以語言的進化和庫的進化需要結(jié)合在一起,這樣用戶就可以第一時間使用這些新特性。流的抽象化是庫的新特性的核心,提供了在數(shù)據(jù)集上進行聚合操作的強大功能,并且和現(xiàn)有的集合類們緊密集成在了一起洲押。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末武花,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子杈帐,更是在濱河造成了極大的恐慌体箕,老刑警劉巖专钉,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異累铅,居然都是意外死亡跃须,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門娃兽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來菇民,“玉大人,你說我怎么就攤上這事投储〉诹罚” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵玛荞,是天一觀的道長娇掏。 經(jīng)常有香客問我,道長勋眯,這世上最難降的妖魔是什么婴梧? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮凡恍,結(jié)果婚禮上志秃,老公的妹妹穿的比我還像新娘。我一直安慰自己嚼酝,他們只是感情好浮还,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著闽巩,像睡著了一般钧舌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涎跨,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天洼冻,我揣著相機與錄音,去河邊找鬼隅很。 笑死撞牢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的叔营。 我是一名探鬼主播屋彪,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绒尊!你這毒婦竟也來了畜挥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤婴谱,失蹤者是張志新(化名)和其女友劉穎蟹但,沒想到半個月后躯泰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡华糖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年麦向,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片客叉。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡磕蛇,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出十办,到底是詐尸還是另有隱情,我是刑警寧澤超棺,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布向族,位于F島的核電站,受9級特大地震影響棠绘,放射性物質(zhì)發(fā)生泄漏件相。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一氧苍、第九天 我趴在偏房一處隱蔽的房頂上張望夜矗。 院中可真熱鬧,春花似錦让虐、人聲如沸紊撕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽对扶。三九已至,卻和暖如春惭缰,著一層夾襖步出監(jiān)牢的瞬間浪南,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工漱受, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留络凿,地道東北人。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓昂羡,卻偏偏與公主長得像絮记,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子紧憾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

推薦閱讀更多精彩內(nèi)容