深入理解Java 8 Lambda(類庫篇)

背景(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 循環的這些特性(串行,依次)是我們所期待的,但它對改善性能造成了阻礙。

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

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

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 類型中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章