Lambda說明:類庫的修改

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

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

背景

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

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

內部 vs 外部迭代(iteration)

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

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

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

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

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

和上面對應的內部迭代的例子如下:

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

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

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

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

流 (Streams)

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

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

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

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

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

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

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

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

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

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

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

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

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

流(Streams) vs集合(Collections)

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

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

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

惰性求值(Laziness)

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

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

在如下的一個管道中 :

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

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

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

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

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

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

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

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

并行化

管道流可以選擇串行或并行執行,除非顯式調用并行流,JDK默認實現返回一個串行流(串行流可以通過parallel方法轉化為并行流)。

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

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

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

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

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

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

舉例說明

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

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把它轉換為一個Stream,然后使用一系列filter過濾掉名字,參數類型和返回類型不匹配的方法。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)十分有效。假設我們有一個"音樂庫"的應用,其中有一系列的專輯,專輯又有它的名字和一系列歌曲,每首歌曲又有它的名字,作者和評分。

假設我們需要找到所有評價在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的方法,返回一個比較器來做比較 (詳細內容請參照"比較器工廠"章節)

收集器(Collectors)

目前為止出現的例子中,我們使用collect方法,傳入一個Collector參數,把stream中的元素收集至一個List或者Set這樣的數據結構中。Collectors這個類包含了很多通用collector的工廠方法,toList和toSet是最常用的兩種,此外還有很多更復雜的對數據轉換的方法。

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

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

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

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

Collectors可以組合和重用產生復雜的收集器。最簡單的groupingBy收集器根據分類函數將元素分組并放入桶(bucket)中,然后再把映射到同一個桶中的元素放入一個List里面。對于使用收集器來組織桶中的元素,我們有一個更通用的版本。我們將分類函數和下游收集器作為參數,依據分類函數分到同一個桶的所有元素都會傳遞給下游收集器。(一個參數的groupingBy方法隱式使用toList方法作為下游收集器)。例如我們如果想把每個作者相關的歌曲收集到Set而不是List中,我們可以使用toSet:

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

如果我們想根據評分和作者創建一個多層的map,我們可以這樣:

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作為分類函數(這里我們忽略單詞的大小寫)并且使用counting收集器作為下游收集器來統計每個單詞的出現頻率(這樣我們不需要創建中間集合):

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

Collector這個類有很多方法構建collector,可用于常見的查詢,匯總和列表,你也可以實現你自己的Collector。

隱藏的并行(Parallelism under the hood)

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

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

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

這種方式把遞歸分解的結構特性和算法分離,而且對于可分解的數據可以并行執行。數據結構的作者只需要提供數據分解的邏輯,就可以立即在stream上并行執行提高效率。

大多數用戶不需要實現Spliterator, 只需要在現有的集合上使用stream等方法。但是如果你需要實現一個集合類或者其他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實現的質量會影響stream執行的效率,返回一個比較均衡分割結果的split方法會提高CPU利用率,如果能提供正確的元數據也會對優化提供幫助。

出現順序(Encounter Order)

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

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

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

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

JDK中的流和Lambda

我們希望通過把Stream的抽象級別提高使得它的特性盡可能廣泛應用于JDK中。Collection已經增加了stream和parallelStream方法來把集合轉換成流,數組可以使用Arrays.stream方法進行轉換。

此外,Stream中有靜態工廠方法來創建流,比如Stream.of, Stream.generate和IntStream.range。還有很多其他的類也增加了Stream相關的方法,比如BufferedReader.lines, Pattern.splitAsStream, Random.ints, 和 BitSet.stream。

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

比較器工廠(Comparator factories)

Comparator 這個類已經添加了一些對于構建比較器十分有用的新方法。

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

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

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

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

這個比老方法清晰很多,通常包含了一個實現了Comparator的匿名內部類實例。但是這種方法真正牛逼的地方在于提高了"組合性"。比如Comparator有一個默認方法來顛倒順序,所以我們如果想以姓的逆序進行排列,我們可以創建和之前一樣的comparator,然后讓它進行逆序:

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

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

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

可變集合操作(Mutative collection operations)

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

總結

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

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

推薦閱讀更多精彩內容