深刻理解Java 8 Lambda(類庫篇——Streams API,Collectors和並行)

轉載:http://zh.lucida.me/blog/java-8-lambdas-inside-out-library-features/

 

關於

  1. 深刻理解 Java 8 Lambda(語言篇——lambda,方法引用,目標類型和默認方法)
  2. 深刻理解 Java 8 Lambda(類庫篇——Streams API,Collector 和並行)
  3. 深刻理解 Java 8 Lambda(原理篇——Java 編譯器如何處理 lambda)

本文是深刻理解 Java 8 Lambda 系列的第二篇,主要介紹 Java 8 針對新增語言特性而新增的類庫(例如 Streams API、Collectors 和並行)。html

本文是對 Brian GoetzState of the Lambda: Libraries Edition 一文的翻譯。java

Java SE 8 增長了新的語言特性(例如 lambda 表達式和默認方法),爲此 Java SE 8 的類庫也進行了不少改進,本文簡要介紹了這些改進。在閱讀本文前,你應該先閱讀 深刻淺出Java 8 Lambda(語言篇),以便對 Java SE 8 的新增特性有一個全面瞭解。編程

背景(Background)

自從lambda表達式成爲Java語言的一部分以後,Java集合(Collections)API就面臨着大幅變化。而 JSR 355(規定了 Java lambda 表達式的標準)的正式啓用更是使得 Java 集合 API 變的過期不堪。儘管咱們能夠從頭實現一個新的集合框架(好比「Collection II」),但取代現有的集合框架是一項很是艱難的工做,由於集合接口滲透了 Java 生態系統的每一個角落,將它們一一換成新類庫須要至關長的時間。所以,咱們決定採起演化的策略(而非推倒重來)以改進集合 API:數組

  • 爲現有的接口(例如 CollectionList 和 Stream)增長擴展方法;
  • 在類庫中增長新的 (stream,即 java.util.stream.Stream)抽象以便進行彙集(aggregation)操做;
  • 改造現有的類型使之能夠提供流視圖(stream view);
  • 改造現有的類型使之能夠容易的使用新的編程模式,這樣用戶就沒必要拋棄使用以久的類庫,例如 ArrayList 和 HashMap(固然這並非說集合 API 會常駐永存,畢竟集合 API 在設計之初並無考慮到 lambda 表達式。咱們可能會在將來的 JDK 中添加一個更現代的集合類庫)。

除了上面的改進,還有一項重要工做就是提供更加易用的並行(Parallelism)庫。儘管 Java 平臺已經對並行和併發提供了強有力的支持,然而開發者在實際工做(將串行代碼並行化)中仍然會碰到不少問題。所以,咱們但願 Java 類庫可以既便於編寫串行代碼也便於編寫並行代碼,所以咱們把編程的重點從具體執行細節(how computation should be formed)轉移到抽象執行步驟(what computation should be perfomed)。除此以外,咱們還須要在將並行變的 容易(easier)和將並行變的 不可見(invisible)之間作出抉擇,咱們選擇了一個折中的路線:提供 顯式(explicit)但 非侵入(unobstrusive)的並行。(若是把並行變的透明,那麼極可能會引入不肯定性(nondeterminism)以及各類數據競爭(data race)問題)安全

內部迭代和外部迭代(Internal vs external iteration)

集合類庫主要依賴於 外部迭代(external iteration)。Collection 實現 Iterable 接口,從而使得用戶能夠依次遍歷集合的元素。好比咱們須要把一個集合中的形狀都設置成紅色,那麼能夠這麼寫:數據結構

1
2
3
for (Shape shape : shapes) {
shape.setColor(RED);
}

這個例子演示了外部迭代:for-each 循環調用 shapes 的 iterator() 方法進行依次遍歷。外部循環的代碼很是直接,但它有以下問題:併發

  • Java 的 for 循環是串行的,並且必須按照集合中元素的順序進行依次處理;
  • 集合框架沒法對控制流進行優化,例如經過排序、並行、短路(short-circuiting)求值以及惰性求值改善性能。

儘管有時 for-each 循環的這些特性(串行,依次)是咱們所期待的,但它對改善性能形成了阻礙。oracle

咱們可使用 內部迭代(internal iteration)替代外部迭代,用戶把對迭代的控制權交給類庫,並向類庫傳遞迭代時所需執行的代碼。app

下面是前例的內部迭代代碼:框架

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

儘管看起來只是一個小小的語法改動,可是它們的實際差異很是巨大。用戶把對操做的控制權交還給類庫,從而容許類庫進行各類各樣的優化(例如亂序執行、惰性求值和並行等等)。總的來講,內部迭代使得外部迭代中不可能實現的優化成爲可能。

外部迭代同時承擔了 作什麼(把形狀設爲紅色)和 怎麼作(獲得 Iterator 實例而後依次遍歷)兩項職責,而內部迭代只負責 作什麼,而把 怎麼作 留給類庫。經過這樣的職責轉變:用戶的代碼會變得更加清晰,而類庫則能夠進行各類優化,從而使全部用戶都從中受益。

流(Stream)

 是 Java SE 8 類庫中新增的關鍵抽象,它被定義於 java.util.stream(這個包裏有若干流類型:Stream<T> 表明對象引用流,此外還有一系列特化(specialization)流,好比 IntStream 表明整形數字流)。每一個流表明一個值序列,流提供一系列經常使用的彙集操做,使得咱們能夠便捷的在它上面進行各類運算。集合類庫也提供了便捷的方式使咱們能夠以操做流的方式使用集合、數組以及其它數據結構。

流的操做能夠被組合成 流水線(Pipeline)。之前面的例子爲例,若是咱們只想把藍色改爲紅色:

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

在 Collection 上調用 stream() 會生成該集合元素的流視圖(stream view),接下來 filter() 操做會產生只包含藍色形狀的流,最後,這些藍色形狀會被 forEach 操做設爲紅色。

若是咱們想把藍色的形狀提取到新的 List 裏,則能夠:

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

collect() 操做會把其接收的元素彙集(aggregate)到一塊兒(這裏是 List),collect() 方法的參數則被用來指定如何進行彙集操做。在這裏咱們使用 toList() 以把元素輸出到 List 中。(如需更多 collect() 方法的細節,請閱讀 Collectors 一節)

若是每一個形狀都被保存在 Box 裏,而後咱們想知道哪一個盒子至少包含一個藍色形狀,咱們能夠這麼寫:

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

map() 操做經過映射函數(這裏的映射函數接收一個形狀,而後返回包含它的盒子)對輸入流裏面的元素進行依次轉換,而後產生新流。

若是咱們須要獲得藍色物體的總重量,咱們能夠這樣表達:

1
2
3
4
5
int sum =
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())
.sum();

這些例子演示了流框架的設計,以及如何使用流框架解決實際問題。

流和集合(Streams vs Collections)

集合和流盡管在表面上看起來很類似,但它們的設計目標是不一樣的:集合主要用來對其元素進行有效(effective)的管理和訪問(access),而流並不支持對其元素進行直接操做或直接訪問,而只支持經過聲明式操做在其上進行運算而後獲得結果。除此以外,流和集合還有一些其它不一樣:

  • 無存儲:流並不存儲值;流的元素源自數據源(多是某個數據結構、生成函數或 I/O 通道等等),經過一系列計算步驟獲得;
  • 自然的函數式風格(Functional in nature):對流的操做會產生一個結果,但流的數據源不會被修改;
  • 惰性求值:多數流操做(包括過濾、映射、排序以及去重)均可以以惰性方式實現。這使得咱們能夠用一遍遍歷完成整個流水線操做,並能夠用短路操做提供更高效的實現;
  • 無需上界(Bounds optional):很多問題均可以被表達爲無限流(infinite stream):用戶不停地讀取流直到滿意的結果出現爲止(好比說,枚舉 完美數 這個操做能夠被表達爲在全部整數上進行過濾)。集合是有限的,但流不是(操做無限流時咱們必需使用短路操做,以確保操做能夠在有限時間內完成);

從API的角度來看,流和集合徹底互相獨立,不過咱們能夠既把集合做爲流的數據源(Collection 擁有 stream() 和 parallelStream() 方法),也能夠經過流產生一個集合(使用前例的 collect() 方法)。Collection 之外的類型也能夠做爲 stream 的數據源,好比JDK中的 BufferedReaderRandom 和 BitSet 已經被改造能夠用作流的數據源,Arrays.stream() 則產生給定數組的流視圖。事實上,任何能夠用 Iterator 描述的對象均可以成爲流的數據源,若是有額外的信息(好比大小、是否有序等特性),庫還能夠進行進一步的優化。

惰性(Laziness)

過濾和映射這樣的操做既能夠被 急性求值(以 filter 爲例,急性求值須要在方法返回前完成對全部元素的過濾),也能夠被 惰性求值(用 Stream 表明過濾結果,當且僅當須要時才進行過濾操做)在實際中進行惰性運算能夠帶來不少好處。好比說,若是咱們進行惰性過濾,咱們就能夠把過濾和流水線裏的其它操做混合在一塊兒,從而不須要對數據進行多遍遍歷。相相似的,若是咱們在一個大型集合裏搜索第一個知足某個條件的元素,咱們能夠在找到後直接中止,而不是繼續處理整個集合。(這一點對無限數據源是很重要,惰性求值對於有限數據源起到的是優化做用,但對無限數據源起到的是決定做用,沒有惰性求值,對無限數據源的操做將沒法終止)

對於過濾和映射這樣的操做,咱們很天然的會把它當成是惰性求值操做,不過它們是否真的是惰性取決於它們的具體實現。另外,像 sum() 這樣生成值的操做和 forEach() 這樣產生反作用的操做都是「自然急性求值」,由於它們必需要產生具體的結果。

如下面的流水線爲例:

1
2
3
4
5
int sum =
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())
.sum();

這裏的過濾操做和映射操做是惰性的,這意味着在調用 sum() 以前,咱們不會從數據源提取任何元素。在 sum 操做開始以後,咱們把過濾、映射以及求和混合在對數據源的一遍遍歷之中。這樣能夠大大減小維持中間結果所帶來的開銷。

大多數循環均可以用數據源(數組、集合、生成函數以及I/O管道)上的聚合操做來表示:進行一系列惰性操做(過濾和映射等操做),而後用一個急性求值操做(forEachtoArray 和 collect 等操做)獲得最終結果——例如過濾—映射—累積,過濾—映射—排序—遍歷等組合操做。惰性操做通常被用來計算中間結果,這在Streams API設計中獲得了很好的體現——與其讓 filter 和 map 返回一個集合,咱們選擇讓它們返回一個新的流。在 Streams API 中,返回流對象的操做都是惰性操做,而返回非流對象的操做(或者無返回值的操做,例如 forEach())都是急性操做。絕大多數狀況下,潛在的惰性操做會被用於聚合,這正是咱們想要的——流水線中的每一輪操做都會接收輸入流中的元素,進行轉換,而後把轉換結果傳給下一輪操做。

在使用這種 數據源—惰性操做—惰性操做—急性操做 流水線時,流水線中的惰性幾乎是不可見的,由於計算過程被夾在數據源和最終結果(或反作用操做)之間。這使得API的可用性和性能獲得了改善。

對於 anyMatch(Predicate) 和 findFirst() 這些急性求值操做,咱們可使用短路(short-circuiting)來終止沒必要要的運算。如下面的流水線爲例:

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

因爲過濾這一步是惰性的,findFirst 在從其上游獲得一個元素以後就會終止,這意味着咱們只會處理這個元素及其以前的元素,而不是全部元素。findFirst() 方法返回 Optional 對象,由於集合中有可能不存在知足條件的元素。Optional 是一種用於描述可缺失值的類型。

在這種設計下,用戶並不須要顯式進行惰性求值,甚至他們都不須要了解惰性求值。類庫本身會選擇最優化的計算方式。

並行(Parallelism)

流水線既能夠串行執行也能夠並行執行,並行或串行是流的屬性。除非你顯式要求使用並行流,不然JDK總會返回串行流。(串行流能夠經過 parallel() 方法被轉化爲並行流)

儘管並行是顯式的,但它並不須要成爲侵入式的。利用 parallelStream(),咱們能夠輕鬆的把以前重量求和的代碼並行化:

1
2
3
4
5
int sum =
shapes.parallelStream()
.filter(s -> s.getColor = BLUE)
.mapToInt(s -> s.getWeight())
.sum();

並行化以後和以前的代碼區別並不大,然而咱們能夠很容易看出它是並行的(此外咱們並不須要本身去實現並行代碼)。

由於流的數據源多是一個可變集合,若是在遍歷流時數據源被修改,就會產生干擾(interference)。因此在進行流操做時,流的數據源應保持不變(held constant)。這個條件並不難維持,若是集合只屬於當前線程,只要 lambda 表達式不修改流的數據源就能夠。(這個條件和遍歷集合時所需的條件類似,若是集合在遍歷時被修改,絕大多數的集合實現都會拋出ConcurrentModificationException)咱們把這個條件稱爲無干擾性(non-interference)。

咱們應避免在傳遞給流方法的 lambda 產生反作用。通常來講,打印調試語句這種輸出變量的操做是安全的,然而在 lambda 表達式裏訪問可變變量就有可能形成數據競爭或是其它意想不到的問題,由於 lambda 在執行時可能會同時運行在多個線程上,於是它們所看到的元素有可能和正常的順序不一致。無干擾性有兩層含義:

  1. 不要干擾數據源;
  2. 不要干擾其它 lambda 表達式,當一個 lambda 在修改某個可變狀態而另外一個 lambda 在讀取該狀態時就會產生這種干擾。

只要知足無干擾性,咱們就能夠安全的進行並行操做並獲得可預測的結果,即使對線程不安全的集合(例如 ArrayList)也是同樣。

實例(Examples)

下面的代碼源自 JDK 中的 Class 類型(getEnclosingMethod 方法),這段代碼會遍歷全部聲明的方法,而後根據方法名稱、返回類型以及參數的數量和類型進行匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for (Method method : enclosingInfo.getEnclosingClass().getDeclaredMethods()) {
if (method.getName().equals(enclosingInfo.getName())) {
Class<?>[] candidateParamClasses = method.getParameterTypes();
if (candidateParamClasses.length == parameterClasses.length) {
boolean matches = true;
for (int i = 0; i < candidateParamClasses.length; i += 1) {
if (!candidateParamClasses[i].equals(parameterClasses[i])) {
matches = false;
break;
}
}
 
if (matches) { // finally, check return type
if (method.getReturnType().equals(returnType)) {
return method;
}
}
}
}
}
throw new InternalError("Enclosing method not found");

經過使用流,咱們不但能夠消除上面代碼裏面全部的臨時變量,還能夠把控制邏輯交給類庫處理。經過反射獲得方法列表以後,咱們利用 Arrays.stream 將它轉化爲 Stream,而後利用一系列過濾器去除類型不符、參數不符以及返回值不符的方法,而後經過調用 findFirst 獲得 Optional<Method>,最後利用 orElseThrow 返回目標值或者拋出異常。

1
2
3
4
5
6
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"));

相對於未使用流的代碼,這段代碼更加緊湊,可讀性更好,也不容易出錯。

流操做特別適合對集合進行查詢操做。假設有一個「音樂庫」應用,這個應用裏每一個庫都有一個專輯列表,每張專輯都有其名稱和音軌列表,每首音軌表都有名稱、藝術家和評分。

假設咱們須要獲得一個按名字排序的專輯列表,專輯列表裏面的每張專輯都至少包含一首四星及四星以上的音軌,爲了構建這個專輯列表,咱們能夠這麼寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Album> favs = new ArrayList<>();
for (Album album : albums) {
boolean hasFavorite = false;
for (Track track : album.tracks) {
if (track.rating >= 4) {
hasFavorite = true;
break;
}
}
if (hasFavorite)
favs.add(album);
}
Collections.sort(favs, new Comparator<Album>() {
public int compare(Album a1, Album a2) {
return a1.name.compareTo(a2.name);
}
});

咱們能夠用流操做來完成上面代碼中的三個主要步驟——識別一張專輯是否包含一首評分大於等於四星的音軌(使用 anyMatch);按名字排序;以及把知足條件的專輯放在一個 List 中:

1
2
3
4
5
List<Album> sortedFavs =
albums.stream()
.filter(a -> a.tracks.anyMatch(t -> (t.rating >= 4)))
.sorted(Comparator.comparing(a -> a.name))
.collect(Collectors.toList());

Compartor.comparing 方法接收一個函數(該函數返回一個實現了 Comparable 接口的排序鍵值),而後返回一個利用該鍵值進行排序的 Comparator(請參考下面的 比較器工廠 一節)。

收集器(Collectors)

在以前的例子中,咱們利用 collect() 方法把流中的元素聚合到 List 或 Set 中。collect() 接收一個類型爲 Collector 的參數,這個參數決定了如何把流中的元素聚合到其它數據結構中。Collectors 類包含了大量經常使用收集器的工廠方法,toList() 和 toSet() 就是其中最多見的兩個,除了它們還有不少收集器,用來對數據進行對複雜的轉換。

Collector 的類型由其輸入類型和輸出類型決定。以 toList() 收集器爲例,它的輸入類型爲 T,輸出類型爲 List<T>toMap 是另一個較爲複雜的 Collector,它有若干個版本。最簡單的版本接收一對函數做爲輸入,其中一個函數用來生成鍵(key),另外一個函數用來生成值(value)。toMap 的輸入類型是 T,輸出類型是 Map<K, V>,其中 K 和 V 分別是前面兩個函數所生成的鍵類型和值類型。(複雜版本的 toMap 收集器則容許你指定目標 Map 的類型或解決鍵衝突)。舉例來講,下面的代碼以目錄數字爲鍵值建立一個倒排索引:

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

groupingBy 是一個與 toMap 相相似的收集器,好比說咱們想要把咱們最喜歡的音樂按歌手列出來,這時咱們就須要這樣的 Collector:它以 Track 做爲輸入,以 Map<Artist, List<Track>> 做爲輸出。groupingBy 收集器就能夠勝任這個工做,它接收分類函數(classification function),而後根據這個函數生成 Map,該 Map 的鍵是分類函數的返回結果,值是該分類下的元素列表。

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

收集器能夠經過組合和複用來生成更加複雜的收集器,簡單版本的 groupingBy 收集器把元素按照分類函數爲每一個元素計算出分類鍵值,而後把輸入元素輸出到對應的分類列表中。除了這個版本,還有一個更加通用(general)的版本容許你使用 其它 收集器來整理輸入元素:它接收一個分類函數以及一個下流(downstream)收集器(單參數版本的 groupingBy 使用 toList() 做爲其默認下流收集器)。舉例來講,若是咱們想把每首歌曲的演唱者收集到 Set 而非 List 中,咱們可使用 toSet 收集器:

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

若是咱們須要按照歌手和評分來管理歌曲,咱們能夠生成多級 Map

1
2
3
4
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() 收集器計算每一個詞出現的次數(從而無需建立中間集合)。

1
2
3
4
5
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 會返回全部歌曲標題裏面的詞,接下來咱們不區分大小寫的把這些詞分組,並把詞頻做爲值(value)儲存。

Collectors 類包含大量的方法,這些方法被用來創造各式各樣的收集器,以便進行查詢、列表(tabulation)和分組等工做,固然你也能夠實現一個自定義 Collector

並行的實質(Parallelism under the hood)

Java SE 7 引入了 Fork/Join 模型,以便高效實現並行計算。不過,經過 Fork/Join 編寫的並行代碼和同功能的串行代碼的差異很是巨大,這使改寫串行代碼變的很是困難。經過提供串行流和並行流,用戶能夠在串行操做和並行操做之間進行便捷的切換(無需重寫代碼),從而使得編寫正確的並行代碼變的更加容易。

爲了實現並行計算,咱們通常要把計算過程遞歸分解(recursive decompose)爲若干步:

  • 把問題分解爲子問題;
  • 串行解決子問題從而獲得部分結果(partial result);
  • 合併部分結果合爲最終結果。

這也是 Fork/Join 的實現原理。

爲了可以並行化任意流上的全部操做,咱們把流抽象爲 SpliteratorSpliterator 是對傳統迭代器概念的一個泛化。分割迭代器(spliterator)既支持順序依次訪問數據,也支持分解數據:就像 Iterator 容許你跳過一個元素而後保留剩下的元素,Spliterator 容許你把輸入元素的一部分(通常來講是一半)轉移(carve off)到另外一個新的 Spliterator 中,而剩下的數據則會被保存在原來的 Spliterator 裏。(這兩個分割迭代器還能夠被進一步分解)除此以外,分割迭代器還能夠提供源的元數據(好比元素的數量,若是已知的話)和其它一系列布爾值特徵(好比說「元素是否被排序」這樣的特徵),Streams 框架能夠利用這些數據來進行優化。

上面的分解方法也一樣適用於其它數據結構,數據結構的做者只須要提供分解邏輯,而後就能夠直接享用並行流操做帶來的遍歷。

大多數用戶無需去實現 Spliterator 接口,由於集合上的 stream() 方法每每就足夠了。但若是你須要實現一個集合或一個流,那麼你可能須要手動實現 Spliterator 接口。Spliterator 接口的API以下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

集合庫中的基礎接口 Collection 和 Iterable 都實現了正確但相對低效的 spliterator() 實現,但派生接口(例如 Set)和具體實現類(例如 ArrayList)均提供了高效的分割迭代器實現。分割迭代器的實現質量會影響到流操做的執行效率;若是在 split() 方法中進行良好(平衡)的劃分,CPU 的利用率會獲得改善;此外,提供正確的特性(characteristics)和大小(size)這些元數據有利於進一步優化。

出現順序(Encounter order)

多數數據結構(例如列表,數組和I/O通道)都擁有 天然出現順序(natural encounter order),這意味着它們的元素出現順序是可預測的。其它的數據結構(例如 HashSet)則沒有一個明肯定義的出現順序(這也是 HashSet 的 Iterator 實現中不保證元素出現順序的緣由)。

是否具備明肯定義的出現順序是 Spliterator 檢查的特性之一(這個特性也被流使用)。除了少數例外(好比 Stream.forEach()和 Stream.findAny()),並行操做通常都會受到出現順序的限制。這意味着下面的流水線:

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

代碼中名字出現的順序必需要和流中的 Person 出現的順序一致。通常來講,這是咱們所期待的結果,並且它對多大多數的流實現都不會形成明顯的性能損耗。從另外的角度來講,若是源數據是 HashSet,那麼上面代碼中名字就能夠以任意順序出現。

JDK 中的流和 lambda(Streams and lambdas in JDK)

Stream 在 Java SE 8 中很是重要,咱們但願能夠在 JDK 中儘量廣的使用 Stream。咱們爲 Collection 提供了 stream() 和 parallelStream(),以便把集合轉化爲流;此外數組能夠經過 Arrays.stream() 被轉化爲流。

除此以外,Stream 中還有一些靜態工廠方法(以及相關的原始類型流實現),這些方法被用來建立流,例如 Stream.of()Stream.generate 以及 IntStream.range。其它的經常使用類型也提供了流相關的方法,例如 String.charsBufferedReader.linesPattern.splitAsStreamRandom.ints 和 BitSet.stream

最後,咱們提供了一系列API用於構建流,類庫的編寫者能夠利用這些API來在流上實現其它彙集操做。實現 Stream 至少須要一個 Iterator,不過若是編寫者還擁有其它元數據(例如數據大小),類庫就能夠經過 Spliterator 提供一個更加高效的實現(就像 JDK 中全部的集合同樣)。

比較器工廠(Comparator factories)

咱們在 Comparator 接口中新增了若干用於生成比較器的實用方法:

靜態方法 Comparator.comparing() 接收一個函數(該函數返回一個實現 Comparable 接口的比較鍵值),返回一個 Comparator,它的實現十分簡潔:

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

咱們把這種方法稱爲 高階函數 ——以函數做爲參數或是返回值的函數。咱們可使用高階函數簡化代碼:

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

這段代碼比「過去的代碼」(通常要定義一個實現 Comparator 接口的匿名類)要簡潔不少。可是它真正的威力在於它大大改進了可組合性(composability)。舉例來講,Comparator 擁有一個用於逆序的默認方法。因而,若是想把列表按照姓進行反序排序,咱們只須要建立一個和以前同樣的比較器,而後調用反序方法便可:

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

與之相似,默認方法 thenComparing 容許你去改進一個已有的 Comparator:在原比較器返回相等的結果時進行進一步比較。下面的代碼演示瞭如何按照姓和名進行排序:

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

可變的集合操做(Mutative collection operation)

集合上的流操做通常會生成一個新的值或集合。不過有時咱們但願就地修改集合,因此咱們爲集合(例如 CollectionList 和 Map)提供了一些新的方法,好比 Iterable.forEach(Consumer)Collection.removeAll(Predicate)List.replaceAll(UnaryOperator)List.sort(Comparator) 和 Map.computeIfAbsent()。除此以外,ConcurrentMap 中的一些非原子方法(例如 replace 和 putIfAbsent)被提高到 Map 之中。

小結(Summary)

引入 lambda 表達式是 Java 語言的巨大進步,但這還不夠——開發者天天都要使用核心類庫,爲了開發者可以儘量方便的使用語言的新特性,語言的演化和類庫的演化是不可分割的。Stream 抽象做爲新增類庫特性的核心,提供了強大的數據集合操做功能,並被深刻整合到現有的集合類和其它的 JDK 類型中。

相關文章
相關標籤/搜索