Lambda說明:類庫的修改

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

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

背景

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

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

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

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

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

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

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

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

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

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

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

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

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

流 (Streams)

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

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

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

Collection的stream方法產(chǎn)生了一個(gè)集合所有元素的流視圖,filter操作接著產(chǎn)生了一個(gè)只含有藍(lán)色形狀的流,我們?cè)偻ㄟ^forEach方法將其涂成紅色。

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

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

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

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

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

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

如果我們想計(jì)算出藍(lán)色形狀的總重量,我們可以這樣:

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

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

流(Streams) vs集合(Collections)

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

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

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

惰性求值(Laziness)

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

無論采用怎樣的實(shí)現(xiàn)方式,像filter或者mapping這樣的操作可以被認(rèn)為是"天然的惰性"。另一方面,求值運(yùn)算如sum, "副作用運(yùn)算"(side-effect-producing)如forEach是"天然的急性",因?yàn)樗麄儽仨毶梢粋€(gè)具體的值。

在如下的一個(gè)管道中 :

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

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

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

在source-lazy-lazy-eager 這種管道中,惰性大多不可見,因?yàn)橛?jì)算過程夾在source和生成結(jié)果的操作之間。在規(guī)模相對(duì)小的API中會(huì)有很好的可用性和不錯(cuò)的性能。

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

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

因?yàn)閒ilter這一步是惰性的,因此findFirst只有在獲得一個(gè)元素以后才會(huì)把它從上游取出。這意味著我們只需要在輸入(filter)應(yīng)用predicate直至找到一個(gè)predicate的結(jié)果是true的元素,而不需要在所有的元素應(yīng)用predicate。findFirst方法返回了一個(gè)Optional,因?yàn)橛锌赡芩性囟疾粷M足條件。Optional描述了一個(gè)可能存在的值。

用戶其實(shí)無需在意惰性,類庫已經(jīng)做好了必要的事情來精簡(jiǎn)運(yùn)算。

并行化

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

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

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

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

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

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

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

舉例說明

以下是JDK中 Class這個(gè)類 getEnclosingMethod 方法的一部分,它遍歷了所有聲明的(declared)方法,匹配方法名,返回方法類型,方法個(gè)數(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,我們可以消除臨時(shí)變量并且把控制流程置于類庫中。我們通過反射獲得方法列表,通過Arrays.stream把它轉(zhuǎn)換為一個(gè)Stream,然后使用一系列filter過濾掉名字,參數(shù)類型和返回類型不匹配的方法。findFirst這個(gè)方法的返回值是一個(gè)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");

這個(gè)版本的代碼更為緊湊,可讀性強(qiáng)而且不容易出錯(cuò)。

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

假設(shè)我們需要找到所有評(píng)價(jià)在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個(gè)主要步驟:

  1. 在專輯中是否存在評(píng)價(jià)在4星以上的歌曲
  2. 對(duì)專輯進(jìn)行排序
  3. 將滿足條件的專輯放到一個(gè)列表中
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 方法利用了一個(gè)Lambda返回的可比較的key的方法,返回一個(gè)比較器來做比較 (詳細(xì)內(nèi)容請(qǐng)參照"比較器工廠"章節(jié))

收集器(Collectors)

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

收集器通過輸入和輸出類型進(jìn)行參數(shù)化。toList的輸入類型是T,輸出類型是List<T>。稍微復(fù)雜一點(diǎn)的Collector是toMap,有幾個(gè)不同的版本。最簡(jiǎn)單的版本是利用一對(duì)(pair)函數(shù),一個(gè)把輸入映射為map中的key,另外一個(gè)把其映射為value。輸入?yún)?shù)是一個(gè)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ù)作者來列出我們喜歡的曲目,我們想要一個(gè)Collector, 歌曲(Track)是入?yún)?生成一個(gè)Map<Artist,List<Track>>,這個(gè)需求和最簡(jiǎn)單的具有g(shù)roupingBy的collector恰好匹配,這個(gè)collector利用一個(gè)分類函數(shù)(classification function)生成一個(gè)map,它的值是一個(gè)對(duì)應(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ù)雜的收集器。最簡(jiǎn)單的groupingBy收集器根據(jù)分類函數(shù)將元素分組并放入桶(bucket)中,然后再把映射到同一個(gè)桶中的元素放入一個(gè)List里面。對(duì)于使用收集器來組織桶中的元素,我們有一個(gè)更通用的版本。我們將分類函數(shù)和下游收集器作為參數(shù),依據(jù)分類函數(shù)分到同一個(gè)桶的所有元素都會(huì)傳遞給下游收集器。(一個(gè)參數(shù)的groupingBy方法隱式使用toList方法作為下游收集器)。例如我們?nèi)绻氚衙總€(gè)作者相關(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ù)評(píng)分和作者創(chuàng)建一個(gè)多層的map,我們可以這樣:

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

最后一個(gè)例子,假設(shè)我們想得到在曲目標(biāo)題中單詞的出現(xiàn)頻率的分布。首先可以使用Stream.flatMap和Pattern.splitAsStream拿到曲目的流,把曲目的名字分解成單詞,再生成一個(gè)單詞的流。然后可以使用groupingBy函數(shù),傳入String.toUpperCase作為分類函數(shù)(這里我們忽略單詞的大小寫)并且使用counting收集器作為下游收集器來統(tǒng)計(jì)每個(gè)單詞的出現(xiàn)頻率(這樣我們不需要?jiǎng)?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方法將一個(gè)把輸入元素映射到流中的函數(shù)作為參數(shù),它將這個(gè)函數(shù)應(yīng)用到每個(gè)輸入元素中,使用生成的流的內(nèi)容替換每個(gè)元素(這里我們認(rèn)為有兩個(gè)操作,首先將流中的每個(gè)元素映射到零個(gè)或者多個(gè)其他元素的流中, 然后把所有的結(jié)果扁平化到一個(gè)流當(dāng)中)。因此flapMap的結(jié)果是一個(gè)包含所有曲目中不同單詞的流。然后把單詞進(jìn)行分組放入桶中,再用counting收集器來獲得桶中單詞出現(xiàn)的次數(shù)。

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

隱藏的并行(Parallelism under the hood)

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

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

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

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

大多數(shù)用戶不需要實(shí)現(xiàn)Spliterator, 只需要在現(xiàn)有的集合上使用stream等方法。但是如果你需要實(shí)現(xiàn)一個(gè)集合類或者其他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實(shí)現(xiàn),但是子接口(比如Set)或者實(shí)現(xiàn)類(比如ArrayList) 利用基礎(chǔ)接口無法獲得的一些信息復(fù)寫了spliterator,使其更加高效。spliterator實(shí)現(xiàn)的質(zhì)量會(huì)影響stream執(zhí)行的效率,返回一個(gè)比較均衡分割結(jié)果的split方法會(huì)提高CPU利用率,如果能提供正確的元數(shù)據(jù)也會(huì)對(duì)優(yōu)化提供幫助。

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

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

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

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

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

JDK中的流和Lambda

我們希望通過把Stream的抽象級(jí)別提高使得它的特性盡可能廣泛應(yīng)用于JDK中。Collection已經(jīng)增加了stream和parallelStream方法來把集合轉(zhuǎn)換成流,數(shù)組可以使用Arrays.stream方法進(jìn)行轉(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給希望在非標(biāo)準(zhǔn)聚合(non-standard aggregates)上使用stream功能的類庫作者。創(chuàng)建流所需的最小"信息"是一個(gè)迭代器,如果可以額外提供元數(shù)據(jù)(比如size),JDK在實(shí)現(xiàn)Spliterator的時(shí)候會(huì)更有效率(就像現(xiàn)有的集合類那樣)

比較器工廠(Comparator factories)

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

Comparator.comparing這個(gè)靜態(tài)方法利用了一個(gè)提取可比較(Comparable)key并且生成一個(gè)比較器的方法,實(shí)現(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));
}

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

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

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

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

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

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

可變集合操作(Mutative collection operations)

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

總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,772評(píng)論 3 422
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,960評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,350評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,549評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,104評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,914評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,089評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,340評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,834評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,106評(píng)論 2 375

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