本文内容
- 引入
- 測試資料
- collect(toList())
- map
- filter
- flatMap
- max 和 min
- reduce
- 整合操作
- 參考資料
Java 8 對核心類庫的改進主要包括集合類的 API 和新引入的流(Stream)。流使得程式員得以站在更高的抽象層次上對集合進行操作。
本文主要介紹 java.util.stream 中 Lambdas 表達式的使用。
下載下傳 Demo
假設有個藝術家的清單集合,後面會給出定義(藝術家包含名字,歌曲,國籍等屬性),在此先借用一下。若計算來自 UK 的藝術家的人數,如下代碼所示:
int count = 0;
for (Artist artist : allArtists) {
if (artist.isFrom("UK")) {
count++;
}
}
很“簡單”、很“标準”的寫法,無非是周遊一遍,如果是來自 UK 的,計數就增 1。
這段代碼是指令式的程式設計,包含太多樣闆代碼,但樣闆代碼模糊了代碼的本意,無法流暢地表達程式員的意圖。總體來看,for 循環會将行為和方法混為一談。個人認為,行為比方法抽象層次要高,一個行為可以由多個方法構成,也就是說,一個行為可能會經由多個方法來完成。
for 循環其實是一個封裝了疊代的文法糖。如下代碼所示:
int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while (iterator.hasNext()) {
Artist artist = iterator.next();
if (artist.isFrom("UK")) {
count++;
}
}
無論如何,上面的方式都不能達到抽象的目的,無法流程地表達意圖,而且,本質上都是一種串行化的操作。
下面用 Java 8 新特性 Stream 的方式實作上面功能,如下代碼所示:
long count = allArtists.stream().filter(artist -> artist.isFrom("UK")).count();
能夠清晰地表達意圖;如果想并行,可以在 Stream 上調用 Parallel 方法,再執行後續操作。
整個過程被分解為兩個更簡單的操作:過濾和計數,看似化簡為繁,多了一步,好像是執行了兩次循環,而事實上,類庫設計更精妙,隻需對藝術家集合疊代一次。
這也是函數式程式設計的思想,假如給定一個名稱清單,有的隻有一個字元。現要求用逗号做分割符,并傳回清單中的名稱,字元串中不包含單字母名稱,每個名稱的首字母都大寫。代碼很容易寫,關鍵在于用什麼思想。這裡本質上執行了三個任務:篩選,清單以消除單字元,将清單中每個名稱的首字母變換 為大寫,然後将清單轉化 為一個字元串。在指令式語言中,不得不為三個任務都使用同一低級機制(對清單進行疊代)。函數式語言将篩選、變換和轉化視為常見操作,是以它們提供給您從不同視角解決問題的方式。
Scala 分别為篩選、變換和轉化概念使用了行業通用的名稱,即 filter、map 和 reduce。你會下接下來的 Java 8 中看到類似的類庫。
Track、Album、Artist 類的具體定義,檢視 Demo。你可以想象,假設你有一張 CD,Album 是專輯,就是這張 CD;一個 Album 包含多個音樂,Track 就是每個音樂,其包含音樂名和時長;Artist 就是張 CD 的藝術家,他可能是個人,也可能是團隊。
package com.example.java8lambdas.data;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import com.example.java8lambdas.base.Album;
import com.example.java8lambdas.base.Artist;
import com.example.java8lambdas.base.Track;
import static java.util.Arrays.asList;
public class SampleData {
public static final Artist johnColtrane = new Artist("John Coltrane", "US");
public static final Artist johnLennon = new Artist("John Lennon", "UK");
public static final Artist paulMcCartney = new Artist("Paul McCartney", "UK");
public static final Artist georgeHarrison = new Artist("George Harrison", "UK");
public static final Artist ringoStarr = new Artist("Ringo Starr", "UK");
public static final List<Artist> membersOfTheBeatles = Arrays.asList(johnLennon, paulMcCartney, georgeHarrison,
ringoStarr);
public static final Artist theBeatles = new Artist("The Beatles", membersOfTheBeatles, "UK");
public static final Album aLoveSupreme = new Album("A Love Supreme",
asList(new Track("Acknowledgement", 467), new Track("Resolution", 442)), asList(johnColtrane));
public static final Album sampleShortAlbum = new Album("sample Short Album", asList(new Track("short track", 30)),
asList(johnColtrane));
public static final Album manyTrackAlbum = new Album(
"sample Short Album", asList(new Track("short track", 30), new Track("short track 2", 30),
new Track("short track 3", 30), new Track("short track 4", 30), new Track("short track 5", 30)),
asList(johnColtrane));
public static Stream<Album> albums = Stream.of(aLoveSupreme);
public static Stream<Artist> threeArtists() {
return Stream.of(johnColtrane, johnLennon, theBeatles);
}
public static List<Artist> getThreeArtists() {
return Arrays.asList(johnColtrane, johnLennon, theBeatles);
}
public static List<Album> getAlbum() {
return Arrays.asList(aLoveSupreme, sampleShortAlbum, manyTrackAlbum);
}
}
由Stream 裡的值生成一個清單,這是一個及早求值操作。很多 Stream 操作都是惰性求值,是以,調用 Stream 上的一系列方法後,還需要最後再調用一個類似 collect 的及早求值方法。
所謂,惰性求值方法,最終不産生新集合的方法,Stream 大部分方法都是惰性求值;而像 count 這樣最終會從 Stream 産生值的方法是及早求值方法。判斷一個操作是惰性求值還是及早求值,隻需看它的傳回值。如果傳回值是 Stream,那麼就是惰性求值;否則,就是及早求值。
Stream.of(johnColtrane, johnLennon, theBeatles).collect(toList())
把三個藝術家變成一個 List<Artist> 集合。
将一種類型的值轉換成另一種類型,也就是,将一個流中的值轉換成一個新的流。若有一個 List<Artist> 清單 artists,則
List<String> collects = artists.stream().map(artist -> artist.getName()).collect(toList());
傳回所有藝術家的名字。注意,傳回的是字元串清單。藝術家包含衆多屬性,但最後我需要的隻是他們的名字。
Java 8 引入了方法引用的概念,是以,上面的代碼也可以寫成,“類名::方法”的形式:
artists.stream().map(Artist::getName).collect(toList());
map,映射,也可以完成資料類型轉換,如下面代碼,将字元串轉換成大寫;将字元串轉換成整型;将十六進制轉換成十進制:
List<String> strArr = Stream.of("a", "b", "c").map(s -> s.toUpperCase()).collect(toList());
Assert.assertEquals(Arrays.asList("A", "B", "C"), strArr);
List<Integer> iArr = Stream.of("1", "2", "3").map(s -> Integer.valueOf(s)).collect(toList());
Assert.assertEquals(Arrays.asList(1, 2, 3), iArr);
List<Integer> iArr2 = Stream.of("0A", "0B", "0C").map(s -> Integer.valueOf(s, 16)).collect(toList());
Assert.assertEquals(Arrays.asList(10, 11, 12), iArr2);
周遊并檢查元素。保留 Stream 中的一些元素,而過濾掉其他的。若有一個 List<Artist> 清單 artists,則
List<Artist> collect = artists.stream().filter(artist -> "UK".equals(artist.getNationality())).collect(toList());
傳回所有國籍為 UK 的藝術家清單。
可用 Stream 替換值,将多個 Stream 連接配接成一個 Stream。若有兩個藝術家 List<Artist> 清單 artists1 和 artists2,則
List<Artist> collect = Stream.of(artists1, artists2).flatMap(numbers -> numbers.stream()).collect(toList());
傳回兩個藝術家清單的集合。
求最大值和最小值。若有三首音樂,則
List<Track> tracks = Arrays.asList(new Track("Bakai", 524), new Track("Violets for Your Furs", 378),
new Track("Time Was", 541));
Track shortestTrack = tracks.stream().min(Comparator.comparing(track -> track.getLength())).get();
傳回時長最小的音樂。
歸約,可以實作從一組值中生成一個值。Stream 的 count、min、max 方法,事實上,都是 reduce 操作。如下所示,累加:
int sum = Stream.of(1, 2, 3).reduce(0, (acc, element) -> acc + element);
下面再看另一個例子。假如,找出某張專輯上所有樂隊的國籍。可将這個問題分解為如下步驟:
- 找出專輯上的所有表演者;
- 分辨哪些表演者是樂隊;
- 找出每個樂隊的國籍;
- 将找到的國籍放入一個集合。
Set<String> collect = album.getMusicians().filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality()).collect(toSet());
說明:
- 藝術家清單既有個人,也有樂隊,可以認為“The”開頭的是樂隊。
- Album 類的 getMusicians 方法傳回的是 Stream 對象。這讓我們看到,暴露 Stream 集合也許要比暴露 List 或 Set 對象更好,其最大優點在于,很好地封裝了内部實作的資料結構。使用者在實際操作中無論如何使用,對不會影響到内部的 List 或 Set。
- Java 終于有 Lambda 表達式啦~Java 8 語言變化——Lambda 表達式和接口類更改
- Java 下一代: 函數式編碼風格——Groovy、Scala 和 Clojure 共享的函數結構及其優勢