扯淡
在準備學 Java8 之前,我以為不會很難。所以,我就決定一邊學 Java8,一邊寫博客。當我準備寫這篇博客的時候,我發現兩件都不容易。如果不是我親身體驗,我也沒法知道一篇博客的背后,作者得付出多少時間和精力。這會讓我在讀每一篇博客時,保持對作者的敬畏之心。
本篇博客會簡單介紹 Stream,以及認識是用 Stream 的 API。當我不知道這篇博客該怎么寫的時候,我想試著從 Stream 的源碼去解讀。然后,我就傻逼似的,屁顛屁顛地去看 Stream 源碼,結果可想而知,我抱著頭頂上的一堆星星去床上思考人生了。Stream 的功能非常強大,給我們這些普通的程序員帶來了極大的方便。這必然意味著,Java 的布道師們會在底層實現一堆邏輯非常復雜的代碼。所以,學習一門新技術的時候,我們應該先知道這門技術是什么,可以干什么,然后熟練掌握,最后深究它背后的實現原理。
認識 Stream
第一眼看見 Stream 這個詞的時候,會誤認為和 Java 的 I/O 流有著不可描述的關系。其實,它們倆就像潘長江和曾志偉并不是兄弟一樣,毫無關系。
其實和 Stream 有關系的是 Java 中的容器(我們經常使用的 List 和 Set 等集合)。在 Java8 之前,我們使用的是增強 for 語法糖和容器自身的 Iterator 來遍歷數據。我們先來看下清單 1 中的代碼,使用增強 for 和 Iterator 來遍歷集合。
清單 1
List<String> strList =
Arrays.asList("a", "b", "c", "d");
//使用增強 for 遍歷集合
for (String str : strList) {
System.out.println(str);
}
//使用 Iterator 遍歷集合
Iterator iterator = strList.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}
清單 1 中的代碼使用了增強 for 語法糖和 Iterator 來遍歷一個集合。其實使用增強 for 和 Iterator 是一回事。因為增強 for 是 Java 為簡化我們使用 Iterator 而提供的一個語法糖,它背后的實現依然是 Iterator。大家可能會發現,無論我們是使用增強 for 還是 Iteraotr 遍歷集合,都需要將集合中的元素一個個的取出來。這種將元素從集合中取出來的迭代方式叫外部迭代。
現在,我們來看下清單 2 中的代碼,使用 Java8 中的 Stream 來遍歷集合。
清單 2
List<String> strList =
Arrays.asList("a", "b", "c", "d");
strList.stream()
.forEach(str -> System.out.println(str));
當大家讀完清單 2 中的代碼后,是不是似乎有點知道 Stream 是干啥的了。通過請單 2 中的代碼,我們只能看出 Stream 有遍歷的集合功能,而且寫法更加簡單,代碼的可讀性也增強了。使用 Stream 遍歷集合的方式叫內部迭代。
Java8 的布道師們利用了面向對象的思想,對容器的存儲數據和遍歷數據進行了解耦。從 Java8 開始,在絕大多數情況下,我們都會使用 Stream 來遍歷集合,容器只用來存儲數據,無需關心遍歷。
按我的理解,Stream 就是一種新的迭代方式,它以一種更加簡單的方式對數據進行處理,讓代碼簡潔易懂。在接下來的內容中,大家會發現使用 Stream 處理數據的代碼,我們能夠清晰讀出代碼的意圖,而且代碼中沒有多余的臨時變量和命令式代碼。同時,使用 Stream 操作數據,并不會改變數據源(可以是容器,可以是數組,也可以是其他類型的數據源)。在多核時代,我們可以使用 Stream 寫出高效以及線程安全的代碼。但這不意味著我們需要寫線程相關的代碼,因為 Stream 在底層已經幫我們實現了。
在接下來使用 Stream 的過程中,我們只需要在 Stream 的每個函數中傳入一個函數,傳入的函數只是用來告訴 Stream 需要做什么,具體該如何做,并不需要我們關心。
使用 Stream API
準備三個類
在使用 Stream API 之前,我們先準備三個類 Artist (創作音樂的個人或團隊),Track (專輯中的一些曲目),Album (專輯,由若干曲目組成)。接下來的內容,都會圍繞這三個類展開。這三個類的具體定義如清單 3, 4, 5 所示。
清單 3
public class Artist {
//藝術家的名字(例如 “縱貫線樂隊”)
private String name;
//樂隊成員(例如 “周華健"),該字段可為空
private String members;
//樂隊來自哪里(例如 “臺灣”)
private String origin;
//省略 set 和 get 方法
}
清單 4
public class Track {
//曲目名稱
private String name;
//省略 set 和 get 方法
}
清單 5
public class Album {
//專輯名
private String name;
//專輯上所有曲目的列表
private List<Track> tracks;
//參與創作本專輯的藝術家列表
private List<Artist> musicians;
//省略 set 和 get 方法
}
案例描述
問題是,找出某張專輯上所有樂隊的國籍。藝術家列表里既有個人,也有樂隊,其中樂隊名以 The 開頭。
該問題可分解為如下幾個步驟:
- 找出專輯上的所有表演者
- 分辨出哪些表演者是樂隊
- 找出每個樂隊的國籍
- 將找出的國籍放入一個集合
使用命令式代碼實現
我們先是使用命令式風格的代碼實現上述案例,代碼如清單 6 所示。
清單 6
//將專輯中的藝術家為樂隊的單獨放入一個集合 bankList
List<Artist> artistList = album.getMusicians();
List<Artist> bankList = new ArrayList<>();
for (Artist artist : artistList) {
if (artist.getName().startsWith("The")) {
bankList.add(artist);
}
}
//找出bankList中每個樂隊的國籍,并將國籍放入originList
List<String> originList = new ArrayList<>();
for (Artist artist : bankList) {
originList.add(artist.getOrigin());
}
清單 6 的代碼存在如下問題:
- 存在多余的臨時變量
- 樣板式代碼掩蓋了關鍵代碼,代碼的可讀性很低
使用 Stream API 實現
我們現在使用 Stream API 來重寫清單 6 中的代碼,如清單 7 所示。
清單 7
Set<String> originList =
album.getMusicians()
.stream()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getOrigin())
.collect(toSet());
當大家看到清單 7 的代碼時候,有沒有很驚喜?反正我是為之跳躍。我先來解讀下這段代碼。其實,當大家熟悉 Stream 的 API 后,一眼就能看出這段代碼的意圖,它簡直就是將問題的每一個小步驟描述了一遍,沒有一點拖泥帶水。首先通過 getMusicians 獲取 album (專輯) 中的藝術家列表;然后使用藝術家列表構建一個 Stream,這里要說的是,所有 Collection 的子類都可以使用 stream 方法來構建一個 Stream,因為 Java8 允許接口中有 default 方法;接著調用 Stream 的 filter 方法,并告訴它篩選出 Stream 中 藝術家名字以 The 開頭的數據,將篩選出的數據組織成一個新的 Stream 返回;緊接著,調用 Stream 的 map 方法,將 Stream 中的藝術家映射為藝術家的國籍,返回新的 Stream;最后,使用 Stream 的 collect 方法生成一個 Set 集合。
經過清單6 中的代碼與清單 7 中的代碼進行比較,我想大家會一致認同,Stream 簡直太好用了,寫出來的代碼簡潔易懂。
結束語
本篇博客只是簡單介紹了 Stream 和 使用 Stream 解決問題的具體案例,在后續博客中,我會更加細致地介紹 Stream。
彩蛋
我今天要給大家介紹的是,我的學長勇哥,這個是他在開源中國的地址 https://my.oschina.net/silence88。勇哥是個完美的 Java 程序員,人帥,會做菜,彈的一手好吉他(他就是因為吉他與我嫂子相識的),會打籃球,最牛逼的還是寫代碼厲害。勇哥的博客值得一讀,你們會看到一個屌絲程序員是如何打怪升級的。勇哥是在大四的第一個學期開始自學 Java 的,他現在順豐的豐巢就職。
感謝大家的閱讀~