Java8新特性第3章(Stream API)

張磊BARON發表於2016-09-17

轉載請註明出處:www.jianshu.com/p/e3ba9a0b7…
歡迎大家關注我的知乎專欄:zhuanlan.zhihu.com/baron


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呼叫shapesiterator()依次遍歷集合中的元素。這種外部迭代有一些問題:

  • for迴圈是序列的,而且必須按照集合中元素的順序依次進行;
  • 集合框架無法對控制流進行優化,例如通過排序、並行、短路求值以及惰性求值改善效能。

    上面這兩個問題我們會在後面的文章中逐步解答。

第二種寫法我們叫內部迭代,兩段程式碼雖然看起來只是語法上的區別,但實際上他們內部的區別其實非常大。使用者把對操作的控制權交還給類庫,從而允許類庫進行各種各樣的優化(例如亂序執行、惰性求值和並行等等)。總的來說,內部迭代使得外部迭代中不可能實現的優化成為可能。

外部迭代同時承擔了做什麼(把形狀設為紅色)和怎麼做(得到Iterator例項然後依次遍歷),而內部迭代只負責做什麼,而把怎麼做留給類庫。這樣程式碼會變得更加清晰,而集合類庫則可以在內部進行各種優化。

1.什麼是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();複製程式碼

2.Stream vs Collection

流(Stream)和集合(Collection)的區別:

  • Collection主要用來對元素進行管理和訪問;
  • Stream並不支援對其元素進行直接操作和直接訪問,而只支援通過宣告式操作在其之上進行運算後得到結果;
  • Stream不儲存值
  • 對Stream的操作會產生一個結果,但是Stream並不會改變資料來源;
  • 大多數Stream的操作(filter,map,sort等)都是以惰性的方式實現的。這使得我們可以使用一次遍歷完成整個流水線操作,並可以用短路操作提供更高效的實現。

3.惰性求值 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()放在對資料來源一次遍歷中。這樣可以大大減少維持中間結果所帶來的開銷。

4.舉個例子?

前面長篇大論的介紹概念實在太枯燥,為了方便大家理解我們用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());複製程式碼

如果大家喜歡這一系列的文章,歡迎關注我的知乎專欄、GitHub、簡書部落格。

相關文章