歡迎關注微信公衆号:BaronTalk
Stream作為Java8的新特性之一,他與Java IO包中的InputStream和OutputStream完全不是一個概念。Java8中的Stream是對集合功能的一種增強,主要用于對集合對象進行各種非常便利高效的聚合和大批量資料的操作。結合Lambda表達式可以極大的提高開發效率和代碼可讀性。
假設我們需要把一個集合中的所有形狀設定成紅色,那麼我們可以這樣寫
for (Shape shape : shapes){
shape.setColor(RED)
}
複制
如果使用Java8擴充後的集合架構則可以這樣寫:
shapes.foreach(s -> s.setColor(RED));
複制
第一種寫法我們叫外部疊代,for-each調用
shapes
的
iterator()
依次周遊集合中的元素。這種外部疊代有一些問題:
- for循環是串行的,而且必須按照集合中元素的順序依次進行;
- 集合架構無法對控制流進行優化,例如通過排序、并行、短路求值以及惰性求值改善性能。
上面這兩個問題我們會在後面的文章中逐漸解答。
第二種寫法我們叫内部疊代,兩段代碼雖然看起來隻是文法上的差別,但實際上他們内部的差別其實非常大。使用者把對操作的控制權交還給類庫,進而允許類庫進行各種各樣的優化(例如亂序執行、惰性求值和并行等等)。總的來說,内部疊代使得外部疊代中不可能實作的優化成為可能。
外部疊代同時承擔了做什麼(把形狀設為紅色)和怎麼做(得到Iterator執行個體然後依次周遊),而内部疊代隻負責做什麼,而把怎麼做留給類庫。這樣代碼會變得更加清晰,而集合類庫則可以在内部進行各種優化。
一、什麼是Stream
Stream不是集合元素,它也不是資料結構、不能儲存資料,它更像一個更進階的
Interator
。Stream提供了強大的資料集合操作功能,并被深入整合到現有的集合類和其它的JDK類型中。流的操作可以被組合成流水線(Pipeline)。拿前面的例子來說,如果我隻想把藍色改成紅色:
shapes.stream()
.filter(s -> s.getColor() == BLUE)
.forEach(s -> s.setColor(RED));
複制
在
Collection
上調用
stream()
會生成該集合元素的流,接下來
filter()
操作會産生隻包含藍色形狀的流,最後,這些藍色形狀會被
forEach
操作設為紅色。
如果我們想把藍色的形狀提取到新的List裡,則可以:
List<Shape> blue = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.collect(Collectors.toList());
複制
collect()
操作會把其接收的元素聚集到一起(這裡是List),
collect()
方法的參數則被用來指定如何進行聚集操作。在這裡我們使用
toList()
以把元素輸出到List中。
如果每個形狀都被儲存在
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();
複制
複制
二、Stream vs Collection
流(Stream)和集合(Collection)的差別:
- Collection主要用來對元素進行管理和通路;
- Stream并不支援對其元素進行直接操作和直接通路,而隻支援通過聲明式操作在其之上進行運算後得到結果;
- Stream不存儲值
- 對Stream的操作會産生一個結果,但是Stream并不會改變資料源;
- 大多數Stream的操作(filter,map,sort等)都是以惰性的方式實作的。這使得我們可以使用一次周遊完成整個流水線操作,并可以用短路操作提供更高效的實作。
三、惰性求值 vs 急性求值
filter()
和
map()
這樣的操作既可以被急性求值(以
filter()
為例,急性求值需要在方法傳回前完成對所有元素的過濾),也可以被惰性求值(用
Stream
代表過濾結果,當且僅當需要時才進行過濾操作)在實際中進行惰性運算可以帶來很多好處。比如說,如果我們進行惰性過濾,我們就可以把過濾和流水線裡的其它操作混合在一起,進而不需要對資料進行多遍周遊。相類似的,如果我們在一個大型集合裡搜尋第一個滿足某個條件的元素,我們可以在找到後直接停止,而不是繼續處理整個集合。(這一點對無限資料源是很重要,惰性求值對于有限資料源起到的是優化作用,但對無限資料源起到的是決定作用,沒有惰性求值,對無限資料源的操作将無法終止)
對于
filter()
和
map()
這樣的操作,我們很自然的會把它當成是惰性求值操作,不過它們是否真的是惰性取決于它們的具體實作。另外,像
sum()
這樣生成值的操作和
forEach()
這樣産生副作用的操作都是天然急性求值,因為它們必須要産生具體的結果。
我們拿下面這段代碼舉例:
int sum = shapes.stream()
.filter(s -> s.getColor() == BLUE)
.mapToInt(s -> s.getWeight())
.sum();
複制
複制
這裡的
filter()
和
map()
都是惰性的,這就意味着在調用
sum()
之前不會從資料源中提取任何元素。在
sum()
操作之後才會把
filter()
、
map()
和
sum()
放在對資料源一次周遊中。這樣可以大大減少維持中間結果所帶來的開銷。
四、舉個栗子?
前面長篇大論的介紹概念實在太枯燥,為了友善大家了解我們用Streams API來實作一個具體的業務場景。
假設我們有一個房源庫項目,這個房源庫中有一系列的小區,每個小區都有小區名和房源清單,每套房子又有價格、面積等屬性。現在我們需要篩選出含有100平米以上房源的小區,并按照小區名排序。
我們先來看看不用Streams API如何實作:
List<Community> result = new ArrayList<>();
for (Community community : communities) {
for (House house : community.houses) {
if (house.area > 100) {
result.add(community);
break;
}
}
}
Collections.sort(result, new Comparator<Community>() {
@Override
public int compare(Community c1, Community c2) {
return c1.name.compareTo(c2.name);
}
});
return result;
複制
如果使用Streams API:
return communities.stream()
.filter(c -> c.houses.stream().anyMatch(h -> h.area>100))
.sorted(Comparator.comparing(c -> c.name))
.collect(Collectors.toList());
複制
如果你喜歡我的文章,就關注下我的公衆号 BaronTalk 、 知乎專欄 或者在 GitHub 上添個 Star 吧!
微信公衆号:BaronTalk
知乎專欄:https://zhuanlan.zhihu.com/baron
GitHub:https://github.com/BaronZ88
個人部落格:http://baronzhang.com)