Lambda說(shuō)明:類(lèi)庫(kù)的修改

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

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

背景

如果Lambda表達(dá)式最初就存在于Java中,類(lèi)似Collections這樣的API就會(huì)與現(xiàn)在截然不同。由于JSR 335會(huì)將Lambda表達(dá)式增加到Java中,這讓Collections這樣的接口顯得更加過(guò)時(shí)!雖然從頭開(kāi)始構(gòu)建一個(gè)全新的集合框架(Collections Framework)這個(gè)想法十分具有誘惑力,但是集合類(lèi)接口貫穿于整個(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)有的類(lèi)來(lái)提供流視圖(stream views),引入新的語(yǔ)法可以讓人們不通過(guò)ArrayLists和HashMaps類(lèi)來(lái)進(jìn)行相應(yīng)操作。(這并不是說(shuō)Collections這樣的輔助類(lèi)永遠(yuǎn)不會(huì)被替代,很顯然除了設(shè)計(jì)不符合Lambda以外,它還有更多其他的限制。一個(gè)更合適的集合類(lèi)框架可能需要考慮到JDK未來(lái)版本的變化和趨勢(shì))

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

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

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

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

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

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

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

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

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

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

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

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

流 (Streams)

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

流操作被鏈接在一起至"管道"(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è)偻ㄟ^(guò)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();

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

流(Streams) vs集合(Collections)

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

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

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

惰性求值(Laziness)

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

無(wú)論采用怎樣的實(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操作是惰性的。這意味著直到開(kāi)始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或者沒(méi)有返回值的操作是急性的。大多數(shù)情況下,潛在的惰性操作應(yīng)用于聚合上,這也是我們希望看到的 -- 每個(gè)階段都會(huì)獲取一個(gè)輸入流,對(duì)其進(jìn)行一些轉(zhuǎn)換,然后將值傳遞到管道的下一階段。

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

一些急性方法,例如anyMatch(Predicate)或者findFirst同樣也可以用來(lái)進(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(mǎn)足條件。Optional描述了一個(gè)可能存在的值。

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

并行化

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

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

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

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

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

最好避免傳入stream中的Lambda表達(dá)式帶來(lái)任何"副作用"(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í)行順序并不一定是他們看起來(lái)的順序。不可干擾不僅針對(duì)數(shù)據(jù)源,同樣也指 不能干擾其它的Lambda,例如在一個(gè)Lambda對(duì)一個(gè)可變數(shù)據(jù)源進(jìn)行修改的時(shí)候,另外一個(gè)Lambda需要讀取它。

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

舉例說(shuō)明

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

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í)變量并且把控制流程置于類(lèi)庫(kù)中。我們通過(guò)反射獲得方法列表,通過(guò)Arrays.stream把它轉(zhuǎn)換為一個(gè)Stream,然后使用一系列filter過(guò)濾掉名字,參數(shù)類(lèi)型和返回類(lèi)型不匹配的方法。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í)查詢(xún)(ad hoc queries)十分有效。假設(shè)我們有一個(gè)"音樂(lè)庫(kù)"的應(yīng)用,其中有一系列的專(zhuān)輯,專(zhuān)輯又有它的名字和一系列歌曲,每首歌曲又有它的名字,作者和評(píng)分。

假設(shè)我們需要找到所有評(píng)價(jià)在4分以上的歌曲所在的專(zhuān)輯,并且按專(zhuān)輯名字排序。我們可以這樣:

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. 在專(zhuān)輯中是否存在評(píng)價(jià)在4星以上的歌曲
  2. 對(duì)專(zhuān)輯進(jìn)行排序
  3. 將滿(mǎn)足條件的專(zhuān)輯放到一個(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è)比較器來(lái)做比較 (詳細(xì)內(nèi)容請(qǐng)參照"比較器工廠"章節(jié))

收集器(Collectors)

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

收集器通過(guò)輸入和輸出類(lèi)型進(jìn)行參數(shù)化。toList的輸入類(lèi)型是T,輸出類(lèi)型是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é)果的類(lèi)型,或者解決映射過(guò)程中出現(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ù)作者來(lái)列出我們喜歡的曲目,我們想要一個(gè)Collector, 歌曲(Track)是入?yún)?生成一個(gè)Map<Artist,List<Track>>,這個(gè)需求和最簡(jiǎn)單的具有g(shù)roupingBy的collector恰好匹配,這個(gè)collector利用一個(gè)分類(lèi)函數(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ù)分類(lèi)函數(shù)將元素分組并放入桶(bucket)中,然后再把映射到同一個(gè)桶中的元素放入一個(gè)List里面。對(duì)于使用收集器來(lái)組織桶中的元素,我們有一個(gè)更通用的版本。我們將分類(lèi)函數(shù)和下游收集器作為參數(shù),依據(jù)分類(lèi)函數(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作為分類(lèi)函數(shù)(這里我們忽略單詞的大小寫(xiě))并且使用counting收集器作為下游收集器來(lái)統(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收集器來(lái)獲得桶中單詞出現(xiàn)的次數(shù)。

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

隱藏的并行(Parallelism under the hood)

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

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

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

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

大多數(shù)用戶(hù)不需要實(shí)現(xiàn)Spliterator, 只需要在現(xiàn)有的集合上使用stream等方法。但是如果你需要實(shí)現(xiàn)一個(gè)集合類(lèi)或者其他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)類(lèi)(比如ArrayList) 利用基礎(chǔ)接口無(wú)法獲得的一些信息復(fù)寫(xiě)了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沒(méi)有定義出現(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

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

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

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

比較器工廠(Comparator factories)

Comparator 這個(gè)類(lèi)已經(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ù)指至少滿(mǎn)足一下一個(gè)條件的函數(shù):
1) 接收一個(gè)或者多個(gè)函數(shù)作為輸入
2) 輸出一個(gè)函數(shù)
利用這個(gè)comparing我們可以減少重復(fù),簡(jiǎn)化客戶(hù)端代碼,例子如下:

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

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

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

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

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中引入了一些新方法來(lái)利用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)度,但是開(kāi)發(fā)者依舊每天使用核心庫(kù)完成工作。所以語(yǔ)言的進(jìn)化和庫(kù)的進(jìn)化需要結(jié)合在一起,這樣用戶(hù)就可以第一時(shí)間使用這些新特性。流的抽象化是庫(kù)的新特性的核心,提供了在數(shù)據(jù)集上進(jìn)行聚合操作的強(qiáng)大功能,并且和現(xiàn)有的集合類(lèi)們緊密集成在了一起。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容