以下內容轉自:
作者:Lucida
微博:@peng_gong
豆瓣:@figure9
原文鏈接:http://zh.lucida.me/blog/java-8-lambdas-insideout-library-features
本文謝絕轉載,如需轉載需征得作者本人同意,謝謝。
-------------------------------------內容分割線---------------------------------------------------------
- 深入理解Java 8 Lambda(語言篇——lambda,方法引用,目標類型和默認方法)
- 深入理解Java 8 Lambda(類庫篇——Streams API,Collector和并行)
- 深入理解Java 8 Lambda(原理篇——Java編譯器如何處理lambda)
本文是深入理解Java 8 Lambda系列的第二篇,主要介紹Java 8針對新增語言特性而新增的類庫(例如Streams API、Collectors和并行)。
本文是對Brian Goetz的State of the Lambda: Libraries Edition一文的翻譯。
關于
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:
- 為現有的接口(例如
Collection
,List
和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
接口,從而使得用戶可以依次遍歷集合的元素。比如我們需要把一個集合中的形狀都設置成紅色,那么可以這么寫:
for (Shape shape : shapes) {shape.setColor(RED); }
這個例子演示了外部迭代:for-each循環調用shapes
的iterator()
方法進行依次遍歷。外部循環的代碼非常直接,但它有如下問題:
- Java的for循環是串行的,而且必須按照集合中元素的順序進行依次處理;
- 集合框架無法對控制流進行優化,例如通過排序、并行、短路(short-circuiting)求值以及惰性求值改善性能。
盡管有時for-each循環的這些特性(串行,依次)是我們所期待的,但它對改善性能造成了阻礙。
我們可以使用內部迭代(internal iteration)替代外部迭代,用戶把對迭代的控制權交給類庫,并向類庫傳遞迭代時所需執行的代碼。
下面是前例的內部迭代代碼:
shapes.forEach(s -> s.setColor(RED));
盡管看起來只是一個小小的語法改動,但是它們的實際差別非常巨大。用戶把對操作的控制權交還給類庫,從而允許類庫進行各種各樣的優化(例如亂序執行、惰性求值和并行等等)。總的來說,內部迭代使得外部迭代中不可能實現的優化成為可能。
外部迭代同時承擔了做什么(把形狀設為紅色)和怎么做(得到Iterator
實例然后依次遍歷)兩項職責,而內部迭代只負責做什么,而把怎么做留給類庫。通過這樣的職責轉變:用戶的代碼會變得更加清晰,而類庫則可以進行各種優化,從而使所有用戶都從中受益。
流(Stream)
流是Java SE 8類庫中新增的關鍵抽象,它被定義于java.util.stream
(這個包里有若干流類型:Stream<T>
代表對象引用流,此外還有一系列特化(specialization)流,比如IntStream
代表整形數字流)。每個流代表一個值序列,流提供一系列常用的聚集操作,使得我們可以便捷的在它上面進行各種運算。集合類庫也提供了便捷的方式使我們可以以操作流的方式使用集合、數組以及其它數據結構。
流的操作可以被組合成流水線(Pipeline)。以前面的例子為例,如果我們只想把藍色改成紅色:
shapes.stream().filter(s -> s.getColor() == BLUE).forEach(s -> s.setColor(RED));
在Collection
上調用stream()
會生成該集合元素的流視圖(stream view),接下來filter()
操作會產生只包含藍色形狀的流,最后,這些藍色形狀會被forEach
操作設為紅色。
如果我們想把藍色的形狀提取到新的List
里,則可以:
List<Shape> blue = shapes.stream().filter(s -> s.getColor() == BLUE).collect(Collectors.toList());
collect()
操作會把其接收的元素聚集(aggregate)到一起(這里是List
),collect()
方法的參數則被用來指定如何進行聚集操作。在這里我們使用toList()
以把元素輸出到List
中。(如需更多collect()
方法的細節,請閱讀Collectors一節)
如果每個形狀都被保存在Box
里,然后我們想知道哪個盒子至少包含一個藍色形狀,我們可以這么寫:
Set<Box> hasBlueShape = shapes.stream().filter(s -> s.getColor() == BLUE).map(s -> s.getContainingBox()).collect(Collectors.toSet());
map()
操作通過映射函數(這里的映射函數接收一個形狀,然后返回包含它的盒子)對輸入流里面的元素進行依次轉換,然后產生新流。
如果我們需要得到藍色物體的總重量,我們可以這樣表達:
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中的BufferedReader
、Random
和BitSet
已經被改造可以用做流的數據源,Arrays.stream()
則產生給定數組的流視圖。事實上,任何可以用Iterator
描述的對象都可以成為流的數據源,如果有額外的信息(比如大小、是否有序等特性),庫還可以進行進一步的優化。
惰性(Laziness)
過濾和映射這樣的操作既可以被急性求值(以filter
為例,急性求值需要在方法返回前完成對所有元素的過濾),也可以被惰性求值(用Stream
代表過濾結果,當且僅當需要時才進行過濾操作)在實際中進行惰性運算可以帶來很多好處。比如說,如果我們進行惰性過濾,我們就可以把過濾和流水線里的其它操作混合在一起,從而不需要對數據進行多遍遍歷。相類似的,如果我們在一個大型集合里搜索第一個滿足某個條件的元素,我們可以在找到后直接停止,而不是繼續處理整個集合。(這一點對無限數據源是很重要,惰性求值對于有限數據源起到的是優化作用,但對無限數據源起到的是決定作用,沒有惰性求值,對無限數據源的操作將無法終止)
對于過濾和映射這樣的操作,我們很自然的會把它當成是惰性求值操作,不過它們是否真的是惰性取決于它們的具體實現。另外,像sum()
這樣生成值的操作和forEach()
這樣產生副作用的操作都是“天然急性求值”,因為它們必須要產生具體的結果。
以下面的流水線為例:
int sum = shapes.stream().filter(s -> s.getColor() == BLUE).mapToInt(s -> s.getWeight()).sum();
這里的過濾操作和映射操作是惰性的,這意味著在調用sum()
之前,我們不會從數據源提取任何元素。在sum
操作開始之后,我們把過濾、映射以及求和混合在對數據源的一遍遍歷之中。這樣可以大大減少維持中間結果所帶來的開銷。
大多數循環都可以用數據源(數組、集合、生成函數以及I/O管道)上的聚合操作來表示:進行一系列惰性操作(過濾和映射等操作),然后用一個急性求值操作(forEach
,toArray
和collect
等操作)得到最終結果——例如過濾—映射—累積,過濾—映射—排序—遍歷等組合操作。惰性操作一般被用來計算中間結果,這在Streams API設計中得到了很好的體現——與其讓filter
和map
返回一個集合,我們選擇讓它們返回一個新的流。在Streams API中,返回流對象的操作都是惰性操作,而返回非流對象的操作(或者無返回值的操作,例如forEach()
)都是急性操作。絕大多數情況下,潛在的惰性操作會被用于聚合,這正是我們想要的——流水線中的每一輪操作都會接收輸入流中的元素,進行轉換,然后把轉換結果傳給下一輪操作。
在使用這種數據源—惰性操作—惰性操作—急性操作流水線時,流水線中的惰性幾乎是不可見的,因為計算過程被夾在數據源和最終結果(或副作用操作)之間。這使得API的可用性和性能得到了改善。
對于anyMatch(Predicate)
和findFirst()
這些急性求值操作,我們可以使用短路(short-circuiting)來終止不必要的運算。以下面的流水線為例:
Optional<Shape> firstBlue = shapes.stream().filter(s -> s.getColor() == BLUE).findFirst();
由于過濾這一步是惰性的,findFirst
在從其上游得到一個元素之后就會終止,這意味著我們只會處理這個元素及其之前的元素,而不是所有元素。findFirst()
方法返回Optional
對象,因為集合中有可能不存在滿足條件的元素。Optional
是一種用于描述可缺失值的類型。
在這種設計下,用戶并不需要顯式進行惰性求值,甚至他們都不需要了解惰性求值。類庫自己會選擇最優化的計算方式。
并行(Parallelism)
流水線既可以串行執行也可以并行執行,并行或串行是流的屬性。除非你顯式要求使用并行流,否則JDK總會返回串行流。(串行流可以通過parallel()
方法被轉化為并行流)
盡管并行是顯式的,但它并不需要成為侵入式的。利用parallelStream()
,我們可以輕松的把之前重量求和的代碼并行化:
int sum = shapes.parallelStream().filter(s -> s.getColor = BLUE).mapToInt(s -> s.getWeight()).sum();
并行化之后和之前的代碼區別并不大,然而我們可以很容易看出它是并行的(此外我們并不需要自己去實現并行代碼)。
因為流的數據源可能是一個可變集合,如果在遍歷流時數據源被修改,就會產生干擾(interference)。所以在進行流操作時,流的數據源應保持不變(held constant)。這個條件并不難維持,如果集合只屬于當前線程,只要lambda表達式不修改流的數據源就可以。(這個條件和遍歷集合時所需的條件相似,如果集合在遍歷時被修改,絕大多數的集合實現都會拋出ConcurrentModificationException
)我們把這個條件稱為無干擾性(non-interference)。
我們應避免在傳遞給流方法的lambda產生副作用。一般來說,打印調試語句這種輸出變量的操作是安全的,然而在lambda表達式里訪問可變變量就有可能造成數據競爭或是其它意想不到的問題,因為lambda在執行時可能會同時運行在多個線程上,因而它們所看到的元素有可能和正常的順序不一致。無干擾性有兩層含義:
- 不要干擾數據源;
- 不要干擾其它lambda表達式,當一個lambda在修改某個可變狀態而另一個lambda在讀取該狀態時就會產生這種干擾。
只要滿足無干擾性,我們就可以安全的進行并行操作并得到可預測的結果,即便對線程不安全的集合(例如ArrayList
)也是一樣。
實例(Examples)
下面的代碼源自JDK中的Class
類型(getEnclosingMethod
方法),這段代碼會遍歷所有聲明的方法,然后根據方法名稱、返回類型以及參數的數量和類型進行匹配:
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 typeif (method.getReturnType().equals(returnType)) {return method;}}}} } throw new InternalError("Enclosing method not found");
通過使用流,我們不但可以消除上面代碼里面所有的臨時變量,還可以把控制邏輯交給類庫處理。通過反射得到方法列表之后,我們利用Arrays.stream
將它轉化為Stream
,然后利用一系列過濾器去除類型不符、參數不符以及返回值不符的方法,然后通過調用findFirst
得到Optional<Method>
,最后利用orElseThrow
返回目標值或者拋出異常。
return Arrays.stream(enclosingInfo.getEnclosingClass().getDeclaredMethods()).filter(m -> Objects.equal(m.getName(), enclosingInfo.getName())).filter(m -> Arrays.equal(m.getParameterTypes(), parameterClasses)).filter(m -> Objects.equals(m.getReturnType(), returnType)).findFirst().orElseThrow(() -> new InternalError("Enclosing method not found"));
相對于未使用流的代碼,這段代碼更加緊湊,可讀性更好,也不容易出錯。
流操作特別適合對集合進行查詢操作。假設有一個“音樂庫”應用,這個應用里每個庫都有一個專輯列表,每張專輯都有其名稱和音軌列表,每首音軌表都有名稱、藝術家和評分。
假設我們需要得到一個按名字排序的專輯列表,專輯列表里面的每張專輯都至少包含一首四星及四星以上的音軌,為了構建這個專輯列表,我們可以這么寫:
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
中:
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
的類型或解決鍵沖突)。舉例來說,下面的代碼以目錄數字為鍵值創建一個倒排索引:
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
的鍵是分類函數的返回結果,值是該分類下的元素列表。
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
收集器:
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
接收一個返回流(這里是歌曲標題里的詞)的函數。它利用這個函數將輸入流中的每個元素轉換為對應的流,然后把這些流拼接到一個流中。所以上面代碼中的flatMap
會返回所有歌曲標題里面的詞,接下來我們不區分大小寫的把這些詞分組,并把詞頻作為值(value)儲存。
Collectors
類包含大量的方法,這些方法被用來創造各式各樣的收集器,以便進行查詢、列表(tabulation)和分組等工作,當然你也可以實現一個自定義Collector
。
并行的實質(Parallelism under the hood)
Java SE 7引入了Fork/Join模型,以便高效實現并行計算。不過,通過Fork/Join編寫的并行代碼和同功能的串行代碼的差別非常巨大,這使改寫串行代碼變的非常困難。通過提供串行流和并行流,用戶可以在串行操作和并行操作之間進行便捷的切換(無需重寫代碼),從而使得編寫正確的并行代碼變的更加容易。
為了實現并行計算,我們一般要把計算過程遞歸分解(recursive decompose)為若干步:
- 把問題分解為子問題;
- 串行解決子問題從而得到部分結果(partial result);
- 合并部分結果合為最終結果。
這也是Fork/Join的實現原理。
為了能夠并行化任意流上的所有操作,我們把流抽象為Spliterator
,Spliterator
是對傳統迭代器概念的一個泛化。分割迭代器(spliterator)既支持順序依次訪問數據,也支持分解數據:就像Iterator
允許你跳過一個元素然后保留剩下的元素,Spliterator
允許你把輸入元素的一部分(一般來說是一半)轉移(carve off)到另一個新的Spliterator
中,而剩下的數據則會被保存在原來的Spliterator
里。(這兩個分割迭代器還可以被進一步分解)除此之外,分割迭代器還可以提供源的元數據(比如元素的數量,如果已知的話)和其它一系列布爾值特征(比如說“元素是否被排序”這樣的特征),Streams框架可以利用這些數據來進行優化。
上面的分解方法也同樣適用于其它數據結構,數據結構的作者只需要提供分解邏輯,然后就可以直接享用并行流操作帶來的遍歷。
大多數用戶無需去實現Spliterator
接口,因為集合上的stream()
方法往往就足夠了。但如果你需要實現一個集合或一個流,那么你可能需要手動實現Spliterator
接口。Spliterator
接口的API如下所示:
public interface Spliterator<T> {// Element accessboolean tryAdvance(Consumer< ? super T> action);void forEachRemaining(Consumer< ? super T> action);// DecompositionSpliterator<T> trySplit();//Optional metadatalong 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()
),并行操作一般都會受到出現順序的限制。這意味著下面的流水線:
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.chars
,BufferedReader.lines
,Pattern.splitAsStream
,Random.ints
和BitSet.stream
。
最后,我們提供了一系列API用于構建流,類庫的編寫者可以利用這些API來在流上實現其它聚集操作。實現Stream
至少需要一個Iterator
,不過如果編寫者還擁有其它元數據(例如數據大小),類庫就可以通過Spliterator
提供一個更加高效的實現(就像JDK中所有的集合一樣)。
比較器工廠(Comparator factories)
我們在Comparator
接口中新增了若干用于生成比較器的實用方法:
靜態方法Comparator.comparing()
接收一個函數(該函數返回一個實現Comparable
接口的比較鍵值),返回一個Comparator
,它的實現十分簡潔:
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)); }
我們把這種方法稱為高階函數——以函數作為參數或是返回值的函數。我們可以使用高階函數簡化代碼:
List<Person> people = ...
people.sort(comparing(p -> p.getLastName()));
這段代碼比“過去的代碼”(一般要定義一個實現Comparator
接口的匿名類)要簡潔很多。但是它真正的威力在于它大大改進了可組合性(composability)。舉例來說,Comparator
擁有一個用于逆序的默認方法。于是,如果想把列表按照姓進行反序排序,我們只需要創建一個和之前一樣的比較器,然后調用反序方法即可:
people.sort(comparing(p -> p.getLastName()).reversed());
與之類似,默認方法thenComparing
允許你去改進一個已有的Comparator
:在原比較器返回相等的結果時進行進一步比較。下面的代碼演示了如何按照姓和名進行排序:
Comparator<Person> c = Comparator.comparing(p -> p.getLastName()).thenComparing(p -> p.getFirstName()); people.sort(c);
可變的集合操作(Mutative collection operation)
集合上的流操作一般會生成一個新的值或集合。不過有時我們希望就地修改集合,所以我們為集合(例如Collection
,List
和Map
)提供了一些新的方法,比如Iterable.forEach(Consumer)
,Collection.removeAll(Predicate)
,List.replaceAll(UnaryOperator)
,List.sort(Comparator)
和Map.computeIfAbsent()
。除此之外,ConcurrentMap
中的一些非原子方法(例如replace
和putIfAbsent
)被提升到Map
之中。
小結(Summary)
引入lambda表達式是Java語言的巨大進步,但這還不夠——開發者每天都要使用核心類庫,為了開發者能夠盡可能方便的使用語言的新特性,語言的演化和類庫的演化是不可分割的。Stream
抽象作為新增類庫特性的核心,提供了強大的數據集合操作功能,并被深入整合到現有的集合類和其它的JDK類型中。
未完待續——
-----------------------分割線------轉載本章完--------------------
以上內容轉自:
作者:Lucida
微博:@peng_gong
豆瓣:@figure9
原文鏈接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language-features
本文謝絕轉載,如需轉載需征得作者本人同意,謝謝。
?