一、Lambda表達(dá)式
- Lambda 表達(dá)式是一個匿名方法,將行為像數(shù)據(jù)一樣進(jìn)行傳遞。
- Lambda 表達(dá)式的常見結(jié)構(gòu):BinaryOperator<Integer> add = (x, y) → x + y。
- 函數(shù)接口指僅具有單個抽象方法的接口,用來表示 Lambda 表達(dá)式的類型。
二、流(stream)
Stream 是用函數(shù)式編程方式在集合類上進(jìn)行復(fù)雜操作的工具。
allArtists.stream().filter(artist -> artist.isFrom("London"));
這行代碼并未做什么實(shí)際性的工作,filter 只刻畫出了 Stream,但沒有產(chǎn)生新的集合。
像filter 這樣只描述 Stream,最終不產(chǎn)生新集合的方法叫作惰性求值方法;而像 count 這樣最終會從 Stream 產(chǎn)生值的方法叫作及早求值方法。
????????判斷一個操作是惰性求值還是及早求值很簡單:只需看它的返回值。如果返回值是 Stream,那么是惰性求值;如果返回值是另一個值或?yàn)榭?,那么就是及早求值。使用這些操作的理想方式就是形成一個惰性求值的鏈,最后用一個及早求值的操作返回想要的結(jié)果,這正是它的合理之處。
1、常用的流操作
1.1、collect(toList())
????????collect(toList()) 方法由 Stream 里的值生成一個列表,是一個及早求值操作。
下面是使用 collect 方法的一個例子:
List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
????????這段程序展示了如何使用 collect(toList()) 方法從 Stream 中生成一個列表。如上文所述,由于很多 Stream 操作都是惰性求值,因此調(diào)用 Stream 上一系列方法之后,還需要最后再調(diào)用一個類似 collect 的及早求值方法。
1.2、map
????????如果有一個函數(shù)可以將一種類型的值轉(zhuǎn)換成另外一種類型,map 操作就可以使用該函數(shù),將一個流中的值轉(zhuǎn)換成一個新的流。
使用 map 操作將字符串轉(zhuǎn)換為大寫形式示例:
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(toList());
????????傳給 map 的 Lambda 表達(dá)式只接受一個 String 類型的參數(shù),返回一個新的 String。參數(shù)和返回值不必屬于同一種類型,但是 Lambda 表達(dá)式必須是 Function 接口的一個實(shí)例,F(xiàn)unction 接口是只包含一個參數(shù)的普通函數(shù)接口。
1.3、filter
????????遍歷數(shù)據(jù)并檢查其中的元素時,可嘗試使用 Stream 中提供的新方法 filter。
List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
????????和 map 很像,filter 接受一個函數(shù)作為參數(shù),該函數(shù)用 Lambda 表達(dá)式表示。該函數(shù)和前面示例中 if 條件判斷語句的功能一樣,如果字符串首字母為數(shù)字,則返回 true。若要重構(gòu)遺留代碼,for 循環(huán)中的 if 條件語句就是一個很強(qiáng)的信號,可用 filter 方法替代。
1.4、flatMap
????????flatMap 方法可用 Stream 替換值,然后將多個 Stream 連接成一個 Stream。
????????前面已介紹過 map 操作,它可用一個新的值代替 Stream 中的值。但有時,用戶希望讓 map操作有點(diǎn)變化,生成一個新的 Stream 對象取而代之。用戶通常不希望結(jié)果是一連串的流,此時 flatMap 最能派上用場。
????????我們看一個簡單的例子。假設(shè)有一個包含多個列表的流,現(xiàn)在希望得到所有數(shù)字的序列。該問題的一個解法如例所示。
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
????????調(diào)用 stream 方法, 將每個列表轉(zhuǎn)換成 Stream 對象, 其余部分由 flatMap 方法處理。flatMap 方法的相關(guān)函數(shù)接口和 map 方法的一樣,都是 Function 接口,只是方法的返回值限定為 Stream 類型罷了。
1.5、max和min
????????Stream 上常用的操作之一是求最大值和最小值。Stream API 中的 max 和 min 操作足以解決這一問題。
????????下例是查找專輯中最短曲目所用的代碼,展示了如何使用 max 和 min 操作。
List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getLength()))
.get();
????????查找 Stream 中的最大或最小元素,首先要考慮的是用什么作為排序的指標(biāo)。以查找專輯中的最短曲目為例,排序的指標(biāo)就是曲目的長度。
????????為了讓 Stream 對象按照曲目長度進(jìn)行排序,需要傳給它一個 Comparator 對象。Java 8 提供了一個新的靜態(tài)方法 comparing,使用它可以方便地實(shí)現(xiàn)一個比較器。放在以前,我們需要比較兩個對象的某項(xiàng)屬性的值,現(xiàn)在只需要提供一個存取方法就夠了。本例中使用getLength 方法。
1.6、reduce
????????reduce 操作可以實(shí)現(xiàn)從一組值中生成一個值。
使用reduce求和示例:
int count = Stream.of(1, 2, 3).reduce(0, (acc, element) -> acc + element);
????????Lambda 表達(dá)式的返回值是最新的 acc,是上一輪 acc 的值和當(dāng)前元素相加的結(jié)果。
2、重構(gòu)遺留代碼
????????為了進(jìn)一步闡釋如何重構(gòu)遺留代碼,本節(jié)將舉例說明如何將一段使用循環(huán)進(jìn)行集合操作的代碼,重構(gòu)成基于 Stream 的操作。重構(gòu)過程中的每一步都能確保代碼通過單元測試,當(dāng)然你也可以自行實(shí)際操作一遍,體驗(yàn)并驗(yàn)證。
????????假定選定一組專輯,找出其中所有長度大于 1 分鐘的曲目名稱。例是遺留代碼,首先初始化一個 Set 對象,用來保存找到的曲目名稱。然后使用 for 循環(huán)遍歷所有專輯,每次循環(huán)中再使用一個 for 循環(huán)遍歷每張專輯上的每首曲目,檢查其長度是否大于 60 秒,如果是,則將該曲目名稱加入 Set 對象。
遺留代碼:找出長度大于 1 分鐘的曲目
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
for(Album album : albums) {
for (Track track : album.getTrackList()) {
if (track.getLength() > 60) {
String name = track.getName();
trackNames.add(name);
}
}
}
return trackNames;
}
????????如果仔細(xì)閱讀上面的這段代碼,就會發(fā)現(xiàn)幾組嵌套的循環(huán)。僅通過閱讀這段代碼很難看出它的編寫目的,那就來重構(gòu)一下(使用流來重構(gòu)該段代碼的方式很多,下面介紹的只是其中一種。事實(shí)上,對 Stream API 越熟悉,就越不需要細(xì)分步驟。之所以在示例中一步一步地重構(gòu),完全是出于幫助大家學(xué)習(xí)的目的,在工作中無需這樣做)。
????????第一步要修改的是 for 循環(huán)。首先使用 Stream 的 forEach 方法替換掉 for 循環(huán),但還是暫時保留原來循環(huán)體中的代碼,這是在重構(gòu)時非常方便的一個技巧。調(diào)用 stream 方法從專輯列表中生成第一個 Stream,同時不要忘了在上一節(jié)已介紹過,getTracks 方法本身就返回一個 Stream 對象。經(jīng)過第一步重構(gòu)后,代碼如例所示。
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.forEach(album -> {
album.getTracks()
.forEach(track -> {
if (track.getLength() > 60) {
String name = track.getName();
trackNames.add(name);
}
});
});
return trackNames;
}
????????在重構(gòu)的第一步中,雖然使用了流,但是并沒有充分發(fā)揮它的作用。事實(shí)上,重構(gòu)后的代碼還不如原來的代碼好——天哪!因此,是時候引入一些更符合流風(fēng)格的代碼了,最內(nèi)層的 forEach 方法正是主要突破口。
????????最內(nèi)層的 forEach 方法有三個功用:找出長度大于 1 分鐘的曲目,得到符合條件的曲目名稱,將曲目名稱加入集合 Set。這就意味著需要三項(xiàng) Stream 操作:找出滿足某種條件的曲目是 filter 的功能,得到曲目名稱則可用 map 達(dá)成,終結(jié)操作可使用 forEach 方法將曲目名稱加入一個集合。用以上三項(xiàng) Stream 操作將內(nèi)部的 forEach 方法拆分后,代碼如例所示。
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.forEach(album -> {
album.getTracks()
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.forEach(name -> trackNames.add(name));
});
return trackNames;
}
????????現(xiàn)在用更符合流風(fēng)格的操作替換了內(nèi)層的循環(huán),但代碼看起來還是冗長繁瑣。將各種流嵌套起來并不理想,最好還是用干凈整潔的順序調(diào)用一些方法。
????????理想的操作莫過于找到一種方法,將專輯轉(zhuǎn)化成一個曲目的 Stream。眾所周知,任何時候想轉(zhuǎn)化或替代代碼,都該使用 map 操作。這里將使用比 map 更復(fù)雜的 flatMap 操作,把多個Stream 合并成一個 Stream 并返回。將 forEach 方法替換成 flatMap 后,代碼如例所示。
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.forEach(name -> trackNames.add(name));
return trackNames;
}
????????上面的代碼中使用一組簡潔的方法調(diào)用替換掉兩個嵌套的 for 循環(huán),看起來清晰很多。然而至此并未結(jié)束,仍需手動創(chuàng)建一個 Set 對象并將元素加入其中,但我們希望看到的是整個計算任務(wù)由一連串的 Stream 操作完成。
????????到目前為止,雖然還未展示轉(zhuǎn)換的方法,但已有類似的操作。就像使用 collect(Collectors.toList()) 可以將 Stream 中的值轉(zhuǎn)換成一個列表,使用 collect(Collectors.toSet()) 可以將Stream 中的值轉(zhuǎn)換成一個集合。因此,將最后的 forEach 方法替換為 collect,并刪掉變量trackNames,代碼如例所示。
public Set<String> findLongTracks(List<Album> albums) {
return albums.stream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.collect(toSet());
}
????????簡而言之,選取一段遺留代碼進(jìn)行重構(gòu),轉(zhuǎn)換成使用流風(fēng)格的代碼。最初只是簡單地使用流,但沒有引入任何有用的流操作。隨后通過一系列重構(gòu),最終使代碼更符合使用流的風(fēng)格。在上述步驟中我們沒有提到一個重點(diǎn),即編寫示例代碼的每一步都要進(jìn)行單元測試,保證代碼能夠正常工作。重構(gòu)遺留代碼時,這樣做很有幫助。
三、如何使用Lambda表達(dá)式
1、@FunctionalInterface
????????每個用作函數(shù)接口的接口都應(yīng)該添加這個注釋。
????????這究竟是什么意思呢? Java 中有一些接口,雖然只含一個方法,但并不是為了使用Lambda 表達(dá)式來實(shí)現(xiàn)的。比如,有些對象內(nèi)部可能保存著某種狀態(tài),使用帶有一個方法的接口可能純屬巧合。java.lang.Comparable 和 java.io.Closeable 就屬于這樣的情況。
????????該注釋會強(qiáng)制 javac 檢查一個接口是否符合函數(shù)接口的標(biāo)準(zhǔn)。如果該注釋添加給一個枚舉類型、類或另一個注釋,或者接口包含不止一個抽象方法,javac 就會報錯。重構(gòu)代碼時,使用它能很容易發(fā)現(xiàn)問題。
2、Optional
????????reduce 方法的一個重點(diǎn)尚未提及:reduce 方法有兩種形式,一種如前面出現(xiàn)的需要有一個初始值,另一種變式則不需要有初始值。沒有初始值的情況下,reduce 的第一步使用Stream 中的前兩個元素。有時,reduce 操作不存在有意義的初始值,這樣做就是有意義的,此時,reduce 方法返回一個 Optional 對象。
????????Optional 是為核心類庫新設(shè)計的一個數(shù)據(jù)類型,用來替換 null 值。人們對原有的 null 值有很多抱怨,甚至連發(fā)明這一概念的 Tony Hoare 也是如此,他曾說這是自己的一個“價值連城的錯誤”。作為一名有影響力的計算機(jī)科學(xué)家就是這樣:雖然連一毛錢也見不到,卻也可以犯一個“價值連城的錯誤”。
????????人們常常使用 null 值表示值不存在,Optional 對象能更好地表達(dá)這個概念。使用 null 代表值不存在的最大問題在于 NullPointerException。一旦引用一個存儲 null 值的變量,程序會立即崩潰。使用 Optional 對象有兩個目的:首先,Optional 對象鼓勵程序員適時檢查變量是否為空,以避免代碼缺陷;其次,它將一個類的 API 中可能為空的值文檔化,這比閱讀實(shí)現(xiàn)代碼要簡單很多。
????????下面我們舉例說明 Optional 對象的 API,從而切身體會一下它的使用方法。使用工廠方法of,可以從某個值創(chuàng)建出一個 Optional 對象。Optional 對象相當(dāng)于值的容器,而該值可以通過 get 方法提取。如例所示。
Optional<String> a = Optional.of("a");
assertEquals("a", a.get());
????????Optional 對象也可能為空,因此還有一個對應(yīng)的工廠方法 empty,另外一個工廠方法ofNullable 則可將一個空值轉(zhuǎn)換成 Optional 對象。例 4-23 展示了這兩個方法,同時展示了第三個方法 isPresent 的用法(該方法表示一個 Optional 對象里是否有值)。
創(chuàng)建一個空的 Optional 對象,并檢查其是否有值:
Optional emptyOptional = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
assertFalse(emptyOptional.isPresent());
assertTrue(a.isPresent());
????????使用 Optional 對象的方式之一是在調(diào)用 get() 方法前,先使用 isPresent 檢查 Optional對象是否有值。使用 orElse 方法則更簡潔,當(dāng) Optional 對象為空時,該方法提供了一個備選值。如果計算備選值在計算上太過繁瑣,即可使用 orElseGet 方法。該方法接受一個Supplier 對象,只有在 Optional 對象真正為空時才會調(diào)用。例 4-24 展示了這兩個方法。
assertEquals("b", emptyOptional.orElse("b"));
assertEquals("c", emptyOptional.orElseGet(() -> "c"));
????????Optional 對象不僅可以用于新的 Java 8 API,也可用于具體領(lǐng)域類中,和普通的類別無二致。當(dāng)試圖避免空值相關(guān)的缺陷,如未捕獲的異常時,可以考慮一下是否可使用 Optional對象。
- 使用為基本類型定制的 Lambda 表達(dá)式和 Stream,如IntStream 可以顯著提升系統(tǒng)性能。
- 默認(rèn)方法是指接口中定義的包含方法體的方法,方法名有default 關(guān)鍵字做前綴。
- 在一個值可能為空的建模情況下,使用Optional 對象能替代使用 null 值。
四、高級集合類和收集器
1、方法引用
????????Lambda 表達(dá)式有一個常見的用法:Lambda 表達(dá)式經(jīng)常調(diào)用參數(shù)。比如想得到藝術(shù)家的姓名,Lambda 的表達(dá)式如下:
artist -> artist.getName()
????????這種用法如此普遍,因此 Java 8 為其提供了一個簡寫語法,叫作方法引用,幫助程序員重用已有方法。用方法引用重寫上面的 Lambda 表達(dá)式,代碼如下:
Artist::getName
????????標(biāo)準(zhǔn)語法為 Classname::methodName。需要注意的是,雖然這是一個方法,但不需要在后面加括號,因?yàn)檫@里并不調(diào)用該方法。我們只是提供了和 Lambda 表達(dá)式等價的一種結(jié)構(gòu),在需要時才會調(diào)用。凡是使用 Lambda 表達(dá)式的地方,就可以使用方法引用。
????????構(gòu)造函數(shù)也有同樣的縮寫形式,如果你想使用 Lambda 表達(dá)式創(chuàng)建一個 Artist 對象,可能會寫出如下代碼:
(name, nationality) -> new Artist(name, nationality)
????????使用方法引用,上述代碼可寫為:
Artist::new
2、元素順序
????????另外一個尚未提及的關(guān)于集合類的內(nèi)容是流中的元素以何種順序排列。讀者可能知道,一些集合類型中的元素是按順序排列的,比如 List;而另一些則是無序的,比如 HashSet。增加了流操作后,順序問題變得更加復(fù)雜。
????????直觀上看,流是有序的,因?yàn)榱髦械脑囟际前错樞蛱幚淼?。這種順序稱為出現(xiàn)順序。出現(xiàn)順序的定義依賴于數(shù)據(jù)源和對流的操作。
????????在一個有序集合中創(chuàng)建一個流時,流中的元素就按出現(xiàn)順序排列:
List<Integer> numbers = asList(1, 2, 3, 4);
List<Integer> sameOrder = numbers.stream()
.collect(toList());
assertEquals(numbers, sameOrder);
3、使用收集器
????????前面我們使用過 collect(toList()),在流中生成列表。顯然,List 是能想到的從流中生成的最自然的數(shù)據(jù)結(jié)構(gòu),但是有時人們還希望從流生成其他值,比如 Map 或 Set,或者你希望定制一個類將你想要的東西抽象出來。
????????前面已經(jīng)講過,僅憑流上方法的簽名,就能判斷出這是否是一個及早求值的操作。reduce操作就是一個很好的例子,但有時人們希望能做得更多。
????????這就是收集器,一種通用的、從流生成復(fù)雜值的結(jié)構(gòu)。只要將它傳給 collect 方法,所有的流就都可以使用它了。
????????標(biāo)準(zhǔn)類庫已經(jīng)提供了一些有用的收集器,讓我們先來看看。本章示例代碼中的收集器都是從 java.util.stream.Collectors 類中靜態(tài)導(dǎo)入的。
3.1、轉(zhuǎn)換成其他集合
????????有一些收集器可以生成其他集合。比如前面已經(jīng)見過的 toList,生成了 java.util.List 類的實(shí)例。還有 toSet 和 toCollection,分別生成 Set 和 Collection 類的實(shí)例。
總有一些時候,需要最終生成一個集合——比如:
- 已有代碼是為集合編寫的,因此需要將流轉(zhuǎn)換成集合傳入;
- 在集合上進(jìn)行一系列鏈?zhǔn)讲僮骱?,最終希望生成一個值;
- 寫單元測試時,需要對某個具體的集合做斷言。
????????通常情況下,創(chuàng)建集合時需要調(diào)用適當(dāng)?shù)臉?gòu)造函數(shù)指明集合的具體類型:
List<Artist> artists = new ArrayList<>();
????????但是調(diào)用 toList 或者 toSet 方法時,不需要指定具體的類型。Stream 類庫在背后自動為你挑選出了合適的類型。后面會講述如何使用 Stream 類庫并行處理數(shù)據(jù),收集并行操作的結(jié)果需要的Set,和對線程安全沒有要求的 Set 類是完全不同的。
????????可能還會有這樣的情況,你希望使用一個特定的集合收集值,而且你可以稍后指定該集合的類型。比如,你可能希望使用 TreeSet,而不是由框架在背后自動為你指定一種類型的Set。此時就可以使用 toCollection,它接受一個函數(shù)作為參數(shù),來創(chuàng)建集合:
stream.collect(toCollection(TreeSet::new));
3.2、轉(zhuǎn)換成值
????????還可以利用收集器讓流生成一個值。maxBy 和 minBy 允許用戶按某種特定的順序生成一個值。例子展示了如何找出成員最多的樂隊(duì)。它使用一個 Lambda 表達(dá)式,將藝術(shù)家映射
為成員數(shù)量,然后定義了一個比較器,并將比較器傳入 maxBy 收集器。
public Optional<Artist> biggestGroup(Stream<Artist> artists) {
Function<Artist,Long> getCount = artist -> artist.getMembers().count();
return artists.collect(maxBy(comparing(getCount)));
}
????????minBy 就如它的方法名,是用來找出最小值的。
????????還有些收集器實(shí)現(xiàn)了一些常用的數(shù)值運(yùn)算。讓我們通過一個計算專輯曲目平均數(shù)的例子來看看,如例所示。
public double averageNumberOfTracks(List<Album> albums) {
return albums.stream()
.collect(averagingInt(album -> album.getTrackList().size()));
}
????????和以前一樣,通過調(diào)用 stream 方法讓集合生成流,然后調(diào)用 collect 方法收集結(jié)果。averagingInt 方法接受一個 Lambda 表達(dá)式作參數(shù),將流中的元素轉(zhuǎn)換成一個整數(shù),然后再計算平均數(shù)。還有和 double 和 long 類型對應(yīng)的重載方法,幫助程序員將元素轉(zhuǎn)換成相應(yīng)類型的值。
3.3、數(shù)據(jù)分塊
????????另外一個常用的流操作是將其分解成兩個集合。假設(shè)有一個藝術(shù)家組成的流,你可能希望將其分成兩個部分,一部分是獨(dú)唱歌手,另一部分是由多人組成的樂隊(duì)??梢允褂脙纱芜^濾操作,分別過濾出上述兩種藝術(shù)家。
????????但是這樣操作起來有問題。首先,為了執(zhí)行兩次過濾操作,需要有兩個流。其次,如果過濾操作復(fù)雜,每個流上都要執(zhí)行這樣的操作,代碼也會變得冗余。
????????幸好我們有這樣一個收集器 partitioningBy,它接受一個流,并將其分成兩部分。它使用 Predicate 對象判斷一個元素應(yīng)該屬于哪個部分,并根據(jù)布爾值返回一個 Map 到列表。因此,對于 true List 中的元素,Predicate 返回 true;對其他 List 中的元素,Predicate 返回 false。
????????使用它,我們就可以將樂隊(duì)(有多個成員)和獨(dú)唱歌手分開了。在本例中,分塊函數(shù)指明藝術(shù)家是否為獨(dú)唱歌手。實(shí)現(xiàn)如例所示。
public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) {
return artists.collect(partitioningBy(artist -> artist.isSolo()));
}
????????也可以使用方法引用代替 Lambda 表達(dá)式,如例所示。
public Map<Boolean, List<Artist>> bandsAndSoloRef(Stream<Artist> artists) {
return artists.collect(partitioningBy(Artist::isSolo));
}
3.4、數(shù)據(jù)分組
????????數(shù)據(jù)分組是一種更自然的分割數(shù)據(jù)操作,與將數(shù)據(jù)分成 ture 和 false 兩部分不同,可以使用任意值對數(shù)據(jù)分組。比如現(xiàn)在有一個由專輯組成的流,可以按專輯當(dāng)中的主唱對專輯分組。代碼如例所示(使用主唱對專輯分組)。
public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
return albums.collect(groupingBy(album -> album.getMainMusician()));
}
????????和其他例子一樣,調(diào)用流的 collect 方法,傳入一個收集器。groupingBy 收集器接受一個分類函數(shù),用來對數(shù)據(jù)分組,就像 partitioningBy 一樣,接受一個Predicate 對象將數(shù)據(jù)分成 ture 和 false 兩部分。我們使用的分類器是一個 Function 對象,和 map 操作用到的一樣。
3.5、字符串
????????很多時候,收集流中的數(shù)據(jù)都是為了在最后生成一個字符串。假設(shè)我們想將參與制作一張專輯的所有藝術(shù)家的名字輸出為一個格式化好的列表,以專輯 Let It Be 為例,期望的輸出為:"[George Harrison, John Lennon, Paul McCartney, Ringo Starr, The Beatles]"。
????????在 Java 8 還未發(fā)布前,實(shí)現(xiàn)該功能的代碼可能通過不斷迭代列表,使用一個 StringBuilder 對象來記錄結(jié)果。每一步都取出一個藝術(shù)家的名字,追加到 StringBuilder對象。
????????Java 8 提供的流和收集器就能寫出更清晰的代碼:
String result =
artists.stream()
.map(Artist::getName)
.collect(Collectors.joining(", ", "[", "]"));
????????這里使用 map 操作提取出藝術(shù)家的姓名,然后使用 Collectors.joining 收集流中的值,該方法可以方便地從一個流得到一個字符串,允許用戶提供分隔符(用以分隔元素)、前綴和后綴。
3.6、組合收集器
????????現(xiàn)在看到的各種收集器已經(jīng)很強(qiáng)大了,但如果將它們組合起來,會變得更強(qiáng)大。
????????之前我們使用主唱將專輯分組,現(xiàn)在來考慮如何計算一個藝術(shù)家的專輯數(shù)量。一個簡單的方案是使用前面的方法對專輯先分組后計數(shù),如例所示。
Map<Artist, List<Album>> albumsByArtist
= albums.collect(groupingBy(album -> album.getMainMusician()));
Map<Artist, Integer> numberOfAlbums = new HashMap<>();
for(Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
numberOfAlbums.put(entry.getKey(), entry.getValue().size());
}
????????這種方式看起來簡單,但卻有點(diǎn)雜亂無章。這段代碼也是命令式的代碼,不能自動適應(yīng)并行化操作。
????????這里實(shí)際上需要另外一個收集器,告訴 groupingBy 不用為每一個藝術(shù)家生成一個專輯列表,只需要對專輯計數(shù)就可以了。幸好,核心類庫已經(jīng)提供了一個這樣的收集器:counting。使用它,可將上述代碼重寫為例 所示的樣子。
public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {
return albums.collect(groupingBy(album -> album.getMainMusician(),
counting()));
}
????????groupingBy 先將元素分成塊,每塊都與分類函數(shù) getMainMusician 提供的鍵值相關(guān)聯(lián),然后使用下游的另一個收集器收集每塊中的元素,最好將結(jié)果映射為一個 Map。
????????mapping 允許在收集器的容器上執(zhí)行類似 map 的操作。但是需要指明使用什么樣的集合類存儲結(jié)果,比如 toList。這些收集器就像烏龜疊羅漢,龜龜相馱以至無窮。
mapping 收集器和 map 方法一樣,接受一個 Function 對象作為參數(shù),經(jīng)過重構(gòu)后的代碼如例:
使用收集器求每個藝術(shù)家的專輯名:
public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums) {
return albums.collect(groupingBy(Album::getMainMusician,
mapping(Album::getName, toList())));
}
????????這兩個例子中我們都用到了第二個收集器,用以收集最終結(jié)果的一個子集。這些收集器叫作下游收集器。收集器是生成最終結(jié)果的一劑配方,下游收集器則是生成部分結(jié)果的配方,主收集器中會用到下游收集器。這種組合使用收集器的方式,使得它們在 Stream 類庫中的作用更加強(qiáng)大。
4、一些細(xì)節(jié)
????????假設(shè)使用 Map<String, Artist> artistCache 定義緩存,我們需要使用費(fèi)時的數(shù)據(jù)庫操作查詢藝術(shù)家信息,代碼可能如例所示。
public Artist getArtist(String name) {
Artist artist = artistCache.get(name);
if (artist == null) {
artist = readArtistFromDB(name);
artistCache.put(name, artist);
}
return artist;
}
????????Java 8 引入了一個新方法 computeIfAbsent,該方法接受一個 Lambda 表達(dá)式,值不存在時使用該 Lambda 表達(dá)式計算新值。使用該方法,可將上述代碼重寫為例所示的形式。
public Artist getArtist(String name) {
return artistCache.computeIfAbsent(name, this::readArtistFromDB);
}
五、數(shù)據(jù)并行化
????????并行化是指為縮短任務(wù)執(zhí)行時間,將一個任務(wù)分解成幾部分,然后并行執(zhí)行。這和順序執(zhí)行的任務(wù)量是一樣的,區(qū)別就像用更多的馬來拉車,花費(fèi)的時間自然減少了。實(shí)際上,和順序執(zhí)行相比,并行化執(zhí)行任務(wù)時,CPU 承載的工作量更大。
????????一種特殊形式的并行化:數(shù)據(jù)并行化。數(shù)據(jù)并行化是指將數(shù)據(jù)分成塊,為每塊數(shù)據(jù)分配單獨(dú)的處理單元。還是拿馬拉車那個例子打比方,就像從車?yán)锶〕鲆恍┴浳?,放到另一輛車上,兩輛馬車都沿著同樣的路徑到達(dá)目的地。
1、并行化流操作
????????并 行 化 操 作 流 只 需 改 變 一 個 方 法 調(diào) 用。 如 果 已 經(jīng) 有 一 個 Stream 對 象, 調(diào) 用 它 的parallel 方法就能讓其擁有并行操作的能力。如果想從一個集合類創(chuàng)建一個流,調(diào)用parallelStream 就能立即獲得一個擁有并行能力的流。
????????讓我們先來看一個具體的例子,例子計算了一組專輯的曲目總長度。它拿到每張專輯的曲目信息,然后得到曲目長度,最后相加得出曲目總長度。
串行化計算專輯曲目長度:
public int serialArraySum() {
return albums.stream()
.flatMap(Album::getTracks)
.mapToInt(Track::getLength)
.sum();
}
????????調(diào)用 parallelStream 方法即能并行處理,如下例所示,剩余代碼都是一樣的,并行化就是這么簡單!
并行化計算專輯曲目長度:
public int parallelArraySum() {
return albums.parallelStream()
.flatMap(Album::getTracks)
.mapToInt(Track::getLength)
.sum();
}
????????讀到這里,大家的第一反應(yīng)可能是立即將手頭代碼中的 stream 方法替換為 parallelStream方法,因?yàn)檫@樣做簡直太簡單了!先別忙,為了將硬件物盡其用,利用好并行化非常重要,但流類庫提供的數(shù)據(jù)并行化只是其中的一種形式。
????????我們先要問自己一個問題:并行化運(yùn)行基于流的代碼是否比串行化運(yùn)行更快?這不是一個簡單的問題?;氐角懊娴睦?,哪種方式花的時間更多取決于串行或并行化運(yùn)行時的環(huán)境。
2、限制
????????之前提到過使用并行流能工作,但這樣說有點(diǎn)無恥。雖然只需一點(diǎn)改動,就能讓已有代碼并行化運(yùn)行,但前提是代碼寫得符合約定。為了發(fā)揮并行流框架的優(yōu)勢,寫代碼時必須遵守一些規(guī)則和限制。
????????之前調(diào)用 reduce 方法,初始值可以為任意值,為了讓其在并行化時能工作正常,初值必須為組合函數(shù)的恒等值。拿恒等值和其他值做 reduce 操作時,其他值保持不變。比如,使用reduce 操作求和,組合函數(shù)為 (acc, element) -> acc + element,則其初值必須為 0, 因?yàn)槿魏螖?shù)字加 0,值不變。
????????reduce 操作的另一個限制是組合操作必須符合結(jié)合律。這意味著只要序列的值不變,組合操作的順序不重要。有點(diǎn)疑惑?別擔(dān)心!請看例子,我們可以改變加法和乘法的順序,但結(jié)果是一樣的。
加法和乘法滿足結(jié)合律:
(4 + 2) + 1 = 4 + (2 + 1) = 7
(4 * 2) * 1 = 4 * (2 * 1) = 8
????????要避免的是持有鎖。流框架會在需要時,自己處理同步操作,因此程序員沒有必要為自己的數(shù)據(jù)結(jié)構(gòu)加鎖。如果你執(zhí)意為流中要使用的數(shù)據(jù)結(jié)構(gòu)加鎖,比如操作的原始集合,那么有可能是自找麻煩。
????????在前面我還解釋過,使用 parallel 方法能輕易將流轉(zhuǎn)換為并行流。如果讀者在閱讀本書的同時,還查看了相應(yīng)的 API,那么可能會發(fā)現(xiàn)還有一個叫 sequential 的方法。在要對流求值時,不能同時處于兩種模式,要么是并行的,要么是串行的。如果同時調(diào)用了 parallel和 sequential 方法,最后調(diào)用的那個方法起效。
3、性能
影響并行流性能的主要因素有 5 個:
- 數(shù)據(jù)大小
????????輸入數(shù)據(jù)的大小會影響并行化處理對性能的提升。將問題分解之后并行化處理,再將結(jié)果合并會帶來額外的開銷。因此只有數(shù)據(jù)足夠大、每個數(shù)據(jù)處理管道花費(fèi)的時間足夠多時,并行化處理才有意義。
- 源數(shù)據(jù)結(jié)構(gòu)
????????每個管道的操作都基于一些初始數(shù)據(jù)源,通常是集合。將不同的數(shù)據(jù)源分割相對容易,這里的開銷影響了在管道中并行處理數(shù)據(jù)時到底能帶來多少性能上的提升。
- 裝箱
????????處理基本類型比處理裝箱類型要快。
- 核的數(shù)量
????????極端情況下,只有一個核,因此完全沒必要并行化。顯然,擁有的核越多,獲得潛在性能提升的幅度就越大。在實(shí)踐中,核的數(shù)量不單指你的機(jī)器上有多少核,更是指運(yùn)行時你的機(jī)器能使用多少核。這也就是說同時運(yùn)行的其他進(jìn)程,或者線程關(guān)聯(lián)性(強(qiáng)制線程在某些核或 CPU 上運(yùn)行)會影響性能。
- 單元處理開銷
????????比如數(shù)據(jù)大小,這是一場并行執(zhí)行花費(fèi)時間和分解合并操作開銷之間的戰(zhàn)爭?;ㄔ诹髦忻總€元素身上的時間越長,并行操作帶來的性能提升越明顯。
????????在底層,并行流還是沿用了 fork/join 框架。fork 遞歸式地分解問題,然后每段并行執(zhí)行,最終由 join 合并結(jié)果,返回最后的值。
4、其他知識點(diǎn):peak
????????遺憾的是,流有一個方法讓你能查看每個值,同時能繼續(xù)操作流。這就是 peek 方法。例使用peek 方法重寫了前面的例子,輸出流中的值,同時避免了重復(fù)的流操作。
使用 peek 方法記錄中間值:文章來源:http://www.zghlxwxcb.cn/news/detail-560479.html
Set<String> nationalities
= album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.peek(nation -> System.out.println("Found nationality: " + nation))
.collect(Collectors.<String>toSet());
????????記錄日志這是 peek 方法的用途之一。為了像調(diào)試循環(huán)那樣一步一步跟蹤,可在 peek 方法中加入斷點(diǎn),這樣就能逐個調(diào)試流中的元素了。
????????此時,peek 方法可知包含一個空的方法體,只要能設(shè)置斷點(diǎn)就行。有一些調(diào)試器不允許在空的方法體中設(shè)置斷點(diǎn),此時,我將值簡單地映射為其本身,這樣就有地方設(shè)置斷點(diǎn)了,雖然這樣做不夠完美,但只要能工作就行。文章來源地址http://www.zghlxwxcb.cn/news/detail-560479.html
到了這里,關(guān)于Java8函數(shù)式編程(Lambda表達(dá)式)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!