Java8 - Stream API快速入門

Java8旨在幫助程序員寫出更好的代碼,
其對核心類庫的改進也是關鍵的一部分,Stream是Java8種處理集合的抽象概念,
它能夠指定你但願對集合的操做,可是執行操做的時間交給具體實現來決定。java

爲何須要Stream?

Java語言中集合是使用最多的API,幾乎每一個Java程序都會用到集合操做,
這裏的Stream和IO中的Stream不一樣,它提供了對集合操做的加強,極大的提升了操做集合對象的便利性。程序員

集合對於大多數編程任務而言都是基本的,爲了解釋集合是怎麼工做,咱們想象一下當下最火的外賣APP,
當咱們點菜的時候須要按照距離價格銷量等進行排序後篩選出本身滿意的菜品。
你可能想選擇距離本身最近的一家店鋪點菜,儘管用集合能夠完成這件事,但集合的操做遠遠算不上完美。編程

假如讓你編寫上面示例中的代碼,你可能會寫出以下:c#

// 店鋪屬性
public class Property {
    String  name;
    // 距離,單位:米
    Integer distance;
    // 銷量,月售
    Integer sales;
    // 價格,這裏簡單起見就寫一個級別表明價格段
    Integer priceLevel;
    public Property(String name, int distance, int sales, int priceLevel) {
        this.name = name;
        this.distance = distance;
        this.sales = sales;
        this.priceLevel = priceLevel;
    }
    // getter setter 省略
}
複製代碼

我想要篩選距離我最近的店鋪,你可能會寫下這樣的代碼:數組

public static void main(String[] args) {
    Property p1 = new Property("叫了個雞", 1000, 500, 2);
    Property p2 = new Property("張三丰餃子館", 2300, 1500, 3);
    Property p3 = new Property("永和大王", 580, 3000, 1);
    Property p4 = new Property("肯德基", 6000, 200, 4);
    List<Property> properties = Arrays.asList(p1, p2, p3, p4);
    Collections.sort(properties, (x, y) -> x.distance.compareTo(y.distance));
    String name = properties.get(0).name;
    System.out.println("距離我最近的店鋪是:" + name);
}
複製代碼

這裏也使用了部分lambda表達式,在Java8以前你可能寫的更痛苦一些。
要是要處理大量元素又該怎麼辦呢?爲了提升性能,你須要並行處理,並利用多核架構。
但寫並行代碼比用迭代器還要複雜,並且調試起來也夠受的!bash

Stream中操做這些東西固然是很是簡單的,小試牛刀:架構

// Stream操做
String name2 = properties.stream()
                .sorted(Comparator.comparingInt(x -> x.distance))
                .findFirst()
                .get().name;
System.out.println("距離我最近的店鋪是:" + name);
複製代碼

新的API對全部的集合操做都提供了生成流操做的方法,寫的代碼也行雲流水,咱們很是簡單的就篩選了離我最近的店鋪。
在後面咱們繼續講解Stream更多的特性和玩法。併發

外部迭代和內部迭代

當你處理集合時,一般會對它進行迭代,而後處理返回的每一個元素。好比我想看看月銷量大於1000的店鋪個數。app

使用for循環進行迭代

int count = 0;
for (Property property : properties) {
    if(property.sales > 1000){
        count++;
    }
}
複製代碼

上面的操做是可行的,可是當每次迭代的時候你須要些不少重複的代碼。將for循環修改成並行執行也很是困難,
須要修改每一個for的實現。函數

從集合背後的原理來看,for循環封裝了迭代的語法糖,首先調用iterator方法,產生一個Iterator對象,
而後控制整個迭代,這就是外部迭代。迭代的過程經過調用Iterator對象的hasNext和next方法完成。

使用迭代器進行計算

int count = 0;
Iterator<Property> iterator = properties.iterator();
while(iterator.hasNext()){
    Property property = iterator.next();
    if(property.sales > 1000){
        count++;
    }
}
複製代碼

而迭代器也是有問題的。它很難抽象出未知的不能操做;此外它本質上仍是串行化的操做,整體來看使用
for循環會將行爲和方法混爲一談。

另外一種辦法是使用內部迭代完成,properties.stream()該方法返回一個Stream而不是迭代器。

使用內部迭代進行計算

long count = properties.stream()
                .filter(p -> p.sales > 1000)
                .count();
複製代碼

上述代碼是經過Stream API完成的,咱們能夠把它理解爲2個步驟:

  1. 找出全部銷量大於1000的店鋪
  2. 計算出店鋪個數

爲了找出銷量大於1000的店鋪,須要先作一次過濾:filter,你能夠看看這個方法的入參就是前面講到的Predicate斷言型函數式接口,
測試一個函數完成後,返回值爲boolean。
因爲Stream API的風格,咱們沒有改變集合的內容,而是描述了Stream的內容,最終調用count()方法計算出Stream
裏包含了多少個過濾以後的對象,返回值爲long。

建立Stream

你已經知道Java8種在Collection接口添加了Stream方法,能夠將任何集合轉換成一個Stream。
若是你操做的是一個數組可使用Stream.of(1, 2, 3)方法將它轉換爲一個流。

也許有人知道JDK7中添加了一些類庫如Files.readAllLines(Paths.get("/home/biezhi/a.txt"))這樣的讀取文件行方法。
List做爲Collection的子類擁有轉換流的方法,那麼咱們讀取這個文本文件到一個字符串變量中將變得更簡潔:

String content = Files.readAllLines(Paths.get("/home/biezhi/a.txt")).stream()
            .collect(Collectors.joining("\n"));
複製代碼

這裏的collect是後面要講解的收集器,對Stream進行了處理後獲得一個文本文件的內容。

JDK8也爲咱們提供了一些便捷的Stream相關類庫:

建立一個流是很簡單的,下面咱們試試用建立好的Stream作一些操做吧。

流操做

java.util.stream.Stream中定義了許多流操做的方法,爲了更好的理解Stream API掌握它經常使用的操做很是重要。
流的操做其實能夠分爲兩類:處理操做聚合操做

  • 處理操做:諸如filter、map等處理操做將Stream一層一層的進行抽離,返回一個流給下一層使用。
  • 聚合操做:從最後一次流中生成一個結果給調用方,foreach只作處理不作返回。

filter

filter看名字也知道是過濾的意思,咱們一般在篩選數據的時候用到,頻率很是高。
filter方法的參數是Predicate<T> predicate即一個從T到boolean的函數。


篩選出距離我在1000米內的店鋪

properties.stream()
            .filter(p -> p.distance < 1000)
複製代碼

篩選出名稱大於5個字的店鋪

properties.stream()
            .filter(p -> p.name.length() > 5);
複製代碼

map

有時候咱們須要將流中處理的數據類型進行轉換,這時候就可使用map方法來完成,將流中的值轉換爲一個新的流。



列出全部店鋪的名稱

properties.stream()
            .map(p -> p.name);
複製代碼

傳給map的lambda表達式接收一個Property類型的參數,返回一個String。
參數和返回值沒必要屬於同一種類型,可是lambda表達式必須是Function接口的一個實例。

flatMap

有時候咱們會遇到提取子流的操做,這種狀況用的很少可是遇到flatMap將變得更容易處理。


例如咱們有一個List<List<String>>結構的數據:

List<List<String>> lists = new ArrayList<>();
lists.add(Arrays.asList("apple", "click"));
lists.add(Arrays.asList("boss", "dig", "qq", "vivo"));
lists.add(Arrays.asList("c#", "biezhi"));
複製代碼

要作的操做是獲取這些數據中長度大於2的單詞個數

lists.stream()
        .flatMap(Collection::stream)
        .filter(str -> str.length() > 2)
        .count();
複製代碼

在不使用flatMap前你可能須要作2次for循環。這裏調用了List的stream方法將每一個列表轉換成Stream對象,
其餘的就和以前的操做同樣。

max和min

Stream中經常使用的操做之一是求最大值和最小值,Stream API 中的max和min操做足以解決這一問題。

咱們須要篩選出價格最低的店鋪:

Property property = properties.stream()
            .max(Comparator.comparingInt(p -> p.priceLevel))
            .get();
複製代碼

查找Stream中的最大或最小元素,首先要考慮的是用什麼做爲排序的指標。
以查找價格最低的店鋪爲例,排序的指標就是店鋪的價格等級

爲了讓Stream對象按照價格等級進行排序,須要傳給它一個Comparator對象。
Java8提供了一個新的靜態方法comparingInt,使用它能夠方便地實現一個比較器。
放在之前,咱們須要比較兩個對象的某項屬性的值,如今只須要提供一個存取方法就夠了。

收集結果

一般咱們處理完流以後想查看一下結果,好比獲取總數,轉換結果,在前面的示例中你發現調用了
filter、map以後沒有下文了,後續的操做應該調用Stream中的collect方法完成。

獲取距離我最近的2個店鋪

List<Property> properties = properties.stream()
            .sorted(Comparator.comparingInt(x -> x.distance))
            .limit(2)
            .collect(Collectors.toList());
複製代碼

獲取全部店鋪的名稱

List<String> names = properties.stream()
                      .map(p -> p.name)
                      .collect(Collectors.toList());
複製代碼

獲取每一個店鋪的價格等級

Map<String, Integer> map = properties.stream()
        .collect(Collectors.toMap(Property::getName, Property::getPriceLevel));
複製代碼

全部價格等級的店鋪列表

Map<Integer, List<Property>> priceMap = properties.stream()
                .collect(Collectors.groupingBy(Property::getPriceLevel));
複製代碼

並行數據處理

並行和併發

併發是兩個任務共享時間段,並行則是兩個任務在同一時間發生,好比運行在多核CPU上。
若是一個程序要運行兩個任務,而且只有一個CPU給它們分配了不一樣的時間片,那麼這就是併發,而不是並行。

並行化是指爲縮短任務執行時間,將一個任務分解成幾部分,而後並行執行。

這和順序執行的任務量是同樣的,區別就像用更多的馬來拉車,花費的時間天然減小了。
實際上,和順序執行相比,並行化執行任務時,CPU承載的工做量更大。

數據並行化是指將數據分紅塊,爲每塊數據分配單獨的處理單元。

仍是拿馬拉車那個例子打比方,就像從車裏取出一些貨物,放到另外一輛車上,兩輛馬車都沿着一樣的路徑到達目的地。

當須要在大量數據上執行一樣的操做時,數據並行化很管用。
它將問題分解爲可在多塊數據上求解的形式,而後對每塊數據執行運算,最後將各數據塊上獲得的結果彙總,從而得到最終答案。

人們常常拿任務並行化和數據並行化作比較,在任務並行化中,線程不一樣,工做各異。
咱們最常遇到的JavaEE應用容器即是任務並行化的例子之一,每一個線程不光能夠爲不一樣用戶服務,
還能夠爲同一個用戶執行不一樣的任務,好比登陸或往購物車添加商品。

Stream並行流

流使得計算變得容易,它的操做也很是簡單,但你須要遵照一些約定。默認狀況下咱們使用集合的stream方法
建立的是一個串行流,你有兩種辦法讓他變成並行流。

  1. 調用Stream對象的parallel方法
  2. 建立流的時候調用parallelStream而不是stream方法

咱們來用具體的例子來解釋串行和並行流

串行化計算

篩選出價格等級小於4,按照距離排序的2個店鋪名

properties.stream()
            .filter(p -> p.priceLevel < 4)
            .sorted(Comparator.comparingInt(Property::getDistance))
            .map(Property::getName)
            .limit(2)
            .collect(Collectors.toList());
複製代碼

調用 parallelStream 方法即能並行處理

properties.parallelStream()
            .filter(p -> p.priceLevel < 4)
            .sorted(Comparator.comparingInt(Property::getDistance))
            .map(Property::getName)
            .limit(2)
            .collect(Collectors.toList());
複製代碼

讀到這裏,你們的第一反應多是當即將手頭代碼中的stream方法替換爲parallelStream方法,
由於這樣作簡直太簡單了!先別忙,爲了將硬件物盡其用,利用好並行化很是重要,但流類庫提供的數據並行化只是其中的一種形式。

咱們先要問本身一個問題:並行化運行基於流的代碼是否比串行化運行更快?這不是一個簡單的問題。
回到前面的例子,哪一種方式花的時間更多取決於串行或並行化運行時的環境。


相關文章
相關標籤/搜索