第一個(gè)問(wèn)題描述:
找出某張專輯上所有樂(lè)隊(duì)的國(guó)籍。藝術(shù)家列表里既有個(gè)人,也有 樂(lè)隊(duì)。利用一點(diǎn)領(lǐng)域知識(shí),假定一般樂(lè)隊(duì)名以定冠詞 The 開(kāi)頭。當(dāng)然這不是絕對(duì)的,但也 差不多。
找出某張專輯上所有樂(lè)隊(duì)的國(guó)籍。藝術(shù)家列表里既有個(gè)人,也有 樂(lè)隊(duì)。假定一般樂(lè)隊(duì)名以定冠詞 The 開(kāi)頭。
首先, 可將這個(gè)問(wèn)題分解為如下幾個(gè)步驟。
- 找出專輯上的所有表演者。
- 分辨出哪些表演者是樂(lè)隊(duì)。
- 找出每個(gè)樂(lè)隊(duì)的國(guó)籍。
- 將找出的國(guó)籍放入一個(gè)集合。
現(xiàn)在,找出每一步對(duì)應(yīng)的 Stream API 就相對(duì)容易了: - Album 類有個(gè) getMusicians 方法,該方法返回一個(gè) Stream 對(duì)象,包含整張專輯中所有的
表演者; - 使用 filter 方法對(duì)表演者進(jìn)行過(guò)濾,只保留樂(lè)隊(duì);
- 使用 map 方法將樂(lè)隊(duì)映射為其所屬國(guó)家;
- 使用 collect(Collectors.toList()) 方法將國(guó)籍放入一個(gè)列表。
最后,整合所有的操作,就得到如下代碼:
Set<String> origins = album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.collect(toSet());
通過(guò)重構(gòu)將現(xiàn)有代碼改寫(xiě)為L(zhǎng)ambada形式:
問(wèn)題描述:假定選定一組專輯,找出其中所有長(zhǎng)度大于 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ì)閱讀上面的這段代碼,就會(huì)發(fā)現(xiàn)幾組嵌套的循環(huán)。僅通過(guò)閱讀這段代碼很難看出 它的編寫(xiě)目的,那就來(lái)重構(gòu)一下(使用流來(lái)重構(gòu)該段代碼的方式很多,下面介紹的只是其 中一種。事實(shí)上,對(duì) Stream API 越熟悉,就越不需要細(xì)分步驟。之所以在示例中一步一步 地重構(gòu),完全是出于幫助大家學(xué)習(xí)的目的,在工作中無(wú)需這樣做)。
第一步要修改的是 for 循環(huán)。首先使用 Stream 的 forEach 方法替換掉 for 循環(huán),但還是暫 時(shí)保留原來(lái)循環(huán)體中的代碼,這是在重構(gòu)時(shí)非常方便的一個(gè)技巧。調(diào)用 stream 方法從專輯 列表中生成第一個(gè) Stream,同時(shí)不要忘了在上一節(jié)已介紹過(guò),getTracks 方法本身就返回 一個(gè) Stream 對(duì)象。經(jīng)過(guò)第一步重構(gòu)后,代碼如下所示:
```
//第一次重構(gòu):找出長(zhǎng)度大于 1 分鐘的曲目
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)中,雖然使用了流,但是并沒(méi)有充分發(fā)揮它的作用。事實(shí)上,重構(gòu)后的代 碼還不如原來(lái)的代碼好——天哪!因此,是時(shí)候引入一些更符合流風(fēng)格的代碼了,最內(nèi)層 的 forEach 方法正是主要突破口。
最內(nèi)層的 forEach 方法有三個(gè)功用:找出長(zhǎng)度大于 1 分鐘的曲目,得到符合條件的曲目名 稱,將曲目名稱加入集合 Set。這就意味著需要三項(xiàng) Stream 操作:找出滿足某種條件的曲 目是 filter 的功能,得到曲目名稱則可用 map 達(dá)成,終結(jié)操作可使用 forEach 方法將曲目
名稱加入一個(gè)集合。用以上三項(xiàng) Stream 操作將內(nèi)部的 forEach 方法拆分后,代碼如下所示:
```
//第二次重構(gòu):找出長(zhǎng)度大于 1 分鐘的曲目
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),但代碼看起來(lái)還是冗長(zhǎng)繁瑣。將各種流嵌 套起來(lái)并不理想,最好還是用干凈整潔的順序調(diào)用一些方法。
理想的操作莫過(guò)于找到一種方法,將專輯轉(zhuǎn)化成一個(gè)曲目的 Stream。眾所周知,任何時(shí)候 想轉(zhuǎn)化或替代代碼,都該使用 map 操作。這里將使用比 map 更復(fù)雜的 flatMap 操作,把多個(gè) Stream 合并成一個(gè) Stream 并返回。將 forEach 方法替換成 flatMap 后,代碼如下所示:
```
//第三次重構(gòu):找出長(zhǎng)度大于 1 分鐘的曲目
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;
}
```
上面的代碼中使用一組簡(jiǎn)潔的方法調(diào)用替換掉兩個(gè)嵌套的 for 循環(huán),看起來(lái)清晰很多。然 而至此并未結(jié)束,仍需手動(dòng)創(chuàng)建一個(gè) Set 對(duì)象并將元素加入其中,但我們希望看到的是整 個(gè)計(jì)算任務(wù)由一連串的 Stream 操作完成。
到目前為止,雖然還未展示轉(zhuǎn)換的方法,但已有類似的操作。就像使用 collect(Collectors. toList()) 可以將 Stream 中的值轉(zhuǎn)換成一個(gè)列表,使用 collect(Collectors.toSet()) 可以將 Stream 中的值轉(zhuǎn)換成一個(gè)集合。因此,將最后的 forEach 方法替換為 collect,并刪掉變量 trackNames,代碼如下所示:
```
//第四次重構(gòu):找出長(zhǎng)度大于 1 分鐘的曲目
public Set<String> findLongTracks(List<Album> albums) {
return albums.stream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.collect(Collectors.toSet());
}
```
簡(jiǎn)而言之,選取一段遺留代碼進(jìn)行重構(gòu),轉(zhuǎn)換成使用流風(fēng)格的代碼。最初只是簡(jiǎn)單地使用流,但沒(méi)有引入任何有用的流操作。隨后通過(guò)一系列重構(gòu),最終使代碼更符合使用流的風(fēng) 格。在上述步驟中我們沒(méi)有提到一個(gè)重點(diǎn),即編寫(xiě)示例代碼的每一步都要進(jìn)行單元測(cè)試, 保證代碼能夠正常工作。重構(gòu)遺留代碼時(shí),這樣做很有幫助。