Java SE基礎鞏固(十六):Stream(流)

1 什麼是Stream(流)

計算機科學中有不少帶「流」的概念,例如字符流,字節流,比特流等等,不多有書籍在講到這些概念的時候會詳解介紹什麼是流,因此有時候會致使讀者感到迷惑,在這裏,我大膽嘗試簡單解釋一下「流」究竟是個什麼東西。java

舉個例子,水流你們都見過吧(不管是水管中的水流,仍是海流或者河流),從微觀的角度看水流,它就是由一個一個的水分子和其餘物質組合造成的(至於怎麼流動的,這就是流體力學的事了,先無論),從一個或者多個流動到一個或者多個目的地,例如你們的生活用水就是從水庫流到各位的家中。在水流動的過程當中,能夠採起一些處理,使得水變得更加潔淨,安全。shell

從這個例子中,不難看到有幾個關鍵詞:分子,源,目的地,處理等。如今,再來看看計算機中的所謂的比特流,比特流裏的「分子」就是一個一個的比特(0和1),源就是計算機自己(從宏觀的角度看),而目的地則是其餘的計算機,在源和目的地之間,咱們一樣能夠加入一些處理操做來處理比特,使得目的地收到的數據是符合需求的。編程

通過這麼一個類比,各位應該大概知道什麼是「流」了吧,如今給出一個比較簡短的定義(來源是《Java8實戰》):「從支持數據處理操做的源生成的元素序列」。看起來不像是人話對吧,不要懼怕,把這句話拆開來看就行了:數組

  • 元素序列。一個一個的分子根據必定的規則排列造成的集合,例如比特流從一個一個比特組成的序列叫作比特序列,字符流中一個一個字符組成的序列叫作字符序列。
  • 數據處理操做。對序列的元素進行處理,例如污水處理等。
  • 源。生成元素數據的機器或者程序。

除此以外,流還有一些特色:安全

  • 在同一地點,不一樣時刻,看到的元素是不一樣的,即流是具備動態性的,錯過了就是錯過了,沒法再次拿出來作處理。
  • 能夠有多個處理操做,某個處理操做的輸出就是下一個處理操做的輸入,就想生產手機的流水線同樣。
  • .....

做爲補充理解,能夠看看下面這張圖,來源也是《Java8 實戰》一書,描述的是集合和流的區別(我的以爲是一個很形象的比喻):數據結構

FXGqfA.png

2 爲何須要流呢

假設有一個需求:如今有一個Car對象的集合,咱們但願從集合中找到並返回全部符合age <= 2條件的對象,而後根據age字段對對象集合進行排序,最後返回對象的brand字段的集合。框架

若是使用傳統的方式編寫代碼,代碼多是下面這樣的:ide

List<Car> filteredCars = new ArrayList<>();
for (Car car : cars) {
    if (car.getAge() <= 2) {
        filteredCars.add(car);
    }
}

Collections.sort(filteredCars, new Comparator<Car>() {
    @Override
    public int compare(Car o1, Car o2) {
        return o1.compareTo(o2);
    }
});

List<String> carNames = new ArrayList<>();
for (Car car : filteredCars) {
    carNames.add(car.getBrand());
}
複製代碼

注意到代碼中使用了一個filteredCars集合,該集合既不是源集合,也不是目標集合,只是一箇中間集合,若是咱們採用這種方式編程,這個中間集合是不得不使用的,但中間集合是有空間消耗的,若是集合數據不少,那麼這個中間集合的影響就會很大。除此以外,這種方式編寫的代碼並不簡潔,若是沒有註釋的話,想要徹底弄清楚這段代碼是在幹什麼應該不是一件容易的事。那有沒有辦法簡化代碼,讓人一看就知道代碼的目的呢?答案是例如Java8的Stream API,下面來看看使用這種新的方式編寫代碼是怎樣的:函數

List<String> carNames = cars.stream().filter((car) -> car.getAge() <= 2)
        .sorted(Car::compareTo)
        .map(Car::getBrand)
        .collect(Collectors.toList());
複製代碼

很是簡單,就四行代碼!(若是你願意,寫成一行也能夠,可是不推薦)並且很是易讀,看了第一行,就能發現:「哦,這裏要作一個filter的操做」,看了第二行就能發現這是一個sorted排序操做,剩下的同理。oop

先不用管stream()是什麼,sorted()是什麼,後面會介紹到。

上述例子表現了Stream的一個優勢,除此以外,Stream還能夠用於應對數據是無限的狀況,例如素數流,偶數流等等,更多的應用沒法在這短短一篇文章中特性,還須要多多修煉!

3 使用Stream API

Stream API的操做不少,例如filter,sorted,map,reduce,collect等等,大體能夠分爲兩類操做:中間操做和終端操做。流通過中間操做後,其輸出仍然能夠做爲下一個操做的輸入,但通過終端操做以後,就沒法繼續進行操做了,因此通常終端操做都是一些具備「聚合」功能的操做,例如collect將流中的數據「收集」成集合或者其餘什麼用於保存數據的數據結構(用戶能夠自定義這個收集操做,也可使用內置的API)。而中間操做通常都是對數據進行「處理」,例如filter用於篩選數據,map用於對數據進行映射,即將數據轉換成另外一種形式等。下圖是經常使用的API:

FXYFED.png

下面我將選擇幾個最經常使用的操做進行介紹:

  • filter。對數據進行過濾操做,參數類型是Predicate,這是一個函數式接口,故能夠傳遞lambda表達式。
  • map。對數據進行映射,參數類型是Function<T,R>,也是一個函數式接口,能夠傳遞lambda表達式。
  • flatMap。扁平化的map,在一些場景下尤爲重要。
  • anyMatch。是一個終端操做,參數是Predicate,語義是一旦找到任何符合條件的元素,就當即返回了。其餘幾個match也相似。
  • forEach。遍歷,比較經常使用,參數是Consumer,也是函數式接口。
  • collect。收集,是數據聚合操做,若是咱們的最終目的是返回一個集合,那麼就可使用這個API。

先給出共用的數據集合以及類:

public class Car implements Comparable<Car> {

    private String brand;

    private Color color;

    private Integer age;

    public Car(String brand, Color color, Integer age) {
        this.brand = brand;
        this.color = color;
        this.age = age;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    @Override
    public String toString() {
        return "Car{" +
                "brand='" + brand + '\'' +
                ", color=" + color +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Car o) {
        return this.getAge() < o.getAge() ? 1 : this.getAge() == o.getAge() ? 0 : -1;
    }

    public enum Color {
        RED,WHITE,PINK,BLACK,BLUE;
    }
    
    //getter and setter
}

複製代碼
private static final List<Car> cars = Arrays.asList(
        new Car("BWM",Car.Color.BLACK, 2),
        new Car("Tesla", Car.Color.WHITE, 1),
        new Car("BENZ", Car.Color.RED, 3),
        new Car("Maserati", Car.Color.BLACK,1),
        new Car("Audi", Car.Color.PINK, 5));
複製代碼

3.1 filter

filter的做用就是篩選數據,若是數據符合條件,就讓他繼續在流裏流動,不然直接取出來,使其離開所在的流。假設如今咱們要篩選cars集合中全部顏色是黑色的car對象,該怎麼作呢?很是簡單,以下所示:

cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
        .forEach(System.out::println);
複製代碼

forEach先不用管,後面會講到。

嘗試一下,驗證一下答案是否是BWM和Maserati呢?filter接受一個參數,參數類型是Predicate<? super T>,在上一篇文章中,我已經介紹了函數式接口以及lambda表達式,因此這裏就再也不贅述了。

3.2 map

Google有一個很著名的大數據框架,即Map-Reduce,若是對Hadoop有一些瞭解的話,應該都知道。Map其實就是一個映射,即將原始數據轉換成另外一種形式。如今咱們繼續上面filter的例子,若是如今我想讓返回的僅僅是篩選後的對象的brand字段集合,該如何作呢?可使用map,以下所示:

cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
        .map(Car::getBrand)
        .forEach(System.out::println);
複製代碼

這裏的map就是將Car對象實例轉換成brand字符串,這個操做是很是有意義的,由於若是若是咱們僅僅須要一個brand字符串,對其餘的根本不關係,又有什麼必要還留着其餘數據來影響後續的處理呢?

3.3 flatMap

這是扁平化的map操做,和普通的map操做最大的不一樣就是flatMap能把流中的某個元素都換成另外一個流,而後把全部的流鏈接起來造成一個新的流。仍是有些難以理解是吧,借用書上的一個例子來講明一下:

String[] arrayOfWords = {"Hello", "World"};
Arrays.stream(arrayOfWords)
        .map(word -> word.split(""))
        .distinct()
        .forEach(System.out::println);
複製代碼

這段代碼的目的是找出全部字母,這些字母是在數組中的單詞裏出現的,例如Helllo,World中出現了H,e,l,l,o,r,d,w這幾個字母。但若是你運行一下上面這段代碼,會發現返回的並非咱們所想的那樣,而是相似這樣的:

[Ljava.lang.String;@15aeb7ab
[Ljava.lang.String;@7b23ec81

複製代碼

即返回是兩個數組,怎麼會這樣呢?簡單剖析一下,map操做的對象類型是String,即每一個單詞,而後調用split()方法,該方法的返回值是Spring[]類型,因此map的返回類型是Stream<String[]>類型,然後面又沒有太多的處理了,故最後forEach遍歷的起始是String[]類型的對象,並非咱們想要的字符串。那麼,怎麼修改呢?答案是:使用flatMap使其扁平化:

Arrays.stream(arrayOfWords)
        .map(word -> word.split(""))
        .flatMap(Arrays::stream)
        .distinct()
        .forEach(System.out::println);
複製代碼

只是在map後面多了一個flatMap操做就能解決問題了嗎?剛剛說了,map的返回值類型是Stream<String[]>,即如今流中的元素類型是String[],flatMap嘗試把String[]數組類的內容展開,即若是數組裏的內容是"Hello",那麼flatMap(Arrays::stream),就把H,e,l,l,o當作新的流,而後再組合成一個新的更大的流。一圖勝千言,來看看具體的分析圖:

FXNrcR.png

3.4 anyMatch

即找到任何一個符合條件的元素就當即返回一個true,若是都沒找到,那麼就返回false。以下所示:

boolean IsExist = cars.stream()
        .anyMatch(car -> car.getAge() <= 2);
複製代碼

很是簡單,很少做解釋了。

3.5 forEach

即遍歷,在Java8之前,咱們要麼顯式的使用迭代器或者用加強for循環的方式,要麼就使用索引的方式(前提是集合支持索引)來遍歷集合的元素,在Java8以後,遍歷集合元素變得更加簡單了,再也不須要顯式的構造for循環,這種方式被稱做「內部迭代」。關於其使用,在上面的例子已經屢次使用到了,這裏就再也不寫例子了,但我想提的是內部迭代的效率和外部顯式迭代的效率對比,網上有不少文章有提到,內部迭代的效率比外部迭代更低,我在個人機器上稍微測試了一下(有預熱),發現確實如此,但並不會低不少。我想表達的是,若是真的對性能特別敏感,那麼用傳統的外部顯式迭代也許會是一個更好的選擇,不然我更推薦基於Stream API的內部迭代,由於它更簡潔,語義更明確(我的見解)。

其實這裏提到的「內部迭代」的概念並不只僅存在於forEach操做,能夠說整個流API的操做都是基於「內部迭代」的,這裏特別說明一下。

3.6 collect

最後就是collect操做了,這是一個「聚合」或者叫作「歸約」操做。其參數是Collector<? super T, A, R> collector類型,但這並非一個函數式接口,在Collectors這個類裏提供了一些經常使用的彙集成集合操做,例如toList就是彙集成一個List,toSet就是彙集成一個Set,同時,這是一個終端操做,一旦調用了該操做,就沒法繼續進行其餘操做了。以下所示:

cars.stream().filter(car -> car.getColor().equals(Car.Color.BLACK))
        .map(Car::getBrand)
        .collect(Collectors.toList());
複製代碼

其實咱們也能夠自定義彙集成其餘更多類型的集合或者一些自定義的數據結構,具體的實現方法能夠參考Collectors裏的幾個方法,就很少說了。

4 小結

本文簡單介紹了什麼是流,爲何要使用流以及如何使用Java8提供的Stream API,但其實Stream API的功能遠不止這樣,還有一些更增強大的功能,例如count()操做等等等等.....Java8除了提供實現好的API,還能夠自定義一些符合用戶需求的功能,這也是Stream API強大的緣由之一。

Stream是一個很是龐大的體系,我這一篇短短几千詞的文章遠遠不能囊括全部。若是本文有什麼地方有錯誤或者不足,真誠的但願您能指出,你們共同進步!

相關文章
相關標籤/搜索