Java8新特性3:Stream1——什麼是Stream,Stream的特性,如何使用Stream,Stream與Collection集合的區別

最近打算寫一個Stream流的系列,這是基於我看完《java8 in action》英文原版 Stream部分的一些總結,如果你看《java8 in action》有點難以理解 ,那麼你可以參考一下我的博客,可以讓你清晰Stream的使用和原理。整個流的系列大概按以下方向寫:

  • 一、Stream的概念和特性,Stream的使用,Stream與Collection集合的區別。
  • 二、讀懂Stream API,多方式創建Stream,學會Stream API的Filter、slic、match、Find、reduce等操作。
  • 三、Stream API的收集器功能,如何使用Stream收集數據
  • 四、瞭解Stream的並行處理原理

ps:博文中的實例代碼,用到 FunctionalInterface 接口的地方都用了lambda或者方法引用,所以,如果你對lambda或者方法引用不熟悉或不瞭解,先看一下下面兩篇博文:

Java8新特性1:lambda表達式入門--由淺入深,從單發步槍邁向自動步槍

Java8新特性2:方法引用--深入理解雙冒號::的使用

一、Stream流 入門。

 1、引入流

我們先看看Stream 流的由來:《java8 in action》的作者對現有的在開發中的集合使用,做了一些思考,並引出了流這個東西(上圖)。大概意思是:

目前我們在幾乎所有開發中都會用到集合,但是目前集合在程序開發中的表現還不夠完美,比如你利用集合處理大量數據時,你不得不面對性能問題,不得不考慮進行並行代碼的編寫,這些工作都是比較繁重的,於是作者便創造了Stream 流。相比較Collection集合來說,Stream在開發中就具有許多獨特的優點,這些優點你可以先不用理解,知道就行,我們會在下面的案例代碼中直觀感受到:

  • 以聲明式的方式處理數據集合——更簡潔,更易讀
  • 可複合——更靈活
  • 可並行——無需寫任何多線程代碼,Stream API自動處理這些問題,性能更好

2、舉個例子,如何用Stream流的編碼方式實現需求

我們先不管Stream的概念,先看看作者舉的一個例子:有一些菜單menu,現在需要你寫一個菜單篩選程序,選出菜單中熱量低於400的菜品的菜名。對於這個簡單需求,我們分兩個方法實現,一個使用jdk8以前的Collection API實現,一個使用jdk8的Stream流實現。我們先簡單看看二者的實現代碼的區別:

菜品的bean:Dish.java

package com.aigov.java8_newfeatures.stream;

import lombok.Data;

/**
 * @author : aigoV
 * @date :2019/10/21
 * 菜單bean
 **/
@Data
public class Dish {
    private final String name;//菜名
    private final boolean vegetarian;//是否是素食
    private final int calories;//熱量,卡路里
    private final Type type;//類型

    public enum Type { MEAT, FISH, OTHER }

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    @Override
    public String toString() {
        return "Dish{" +
                "name='" + name + '\'' +
                '}';
    }
}

需求實現類:simpleStream.java

package com.aigov.java8_newfeatures.stream;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author : aigoV
 * @date :2019/10/21
 * Stream流
 **/
public class SimpleStream {

    /**
     * 使用傳統的集合方式
     **/
    public static List<String> getDishNamesByCollections(List<Dish> menu) {
        List<Dish> lowCalories = new ArrayList<>();
        //filter 400  過濾
        for (Dish d : menu) {
            if (d.getCalories() < 400) {
                lowCalories.add(d);
            }
        }
        //sort  排序
        Collections.sort(lowCalories, Comparator.comparingInt(Dish::getCalories));
        //nameList for Calories<400  將菜品名放入一個集合
        List<String> dishNameList = new ArrayList<>();
        for (Dish d : lowCalories) {
            dishNameList.add(d.getName());
        }
        return dishNameList;
    }

    /**
     * 用時java8 Stream流的方式處理數據集
     * @param menu
     * @return
     */
    public static List<String> getDishNamesByStream(List<Dish> menu){

        List<String> getDishNamesByStream = menu.parallelStream()//轉爲可利用多核架構並行執行的Stream
                .filter(d -> d.getCalories()<400)//過濾卡路里小於400的菜
                .sorted(Comparator.comparing(Dish::getCalories))//將過濾後的Stream按卡路里 小->大 排序
                .map(Dish::getName)//提取過濾後的菜品流中的菜名
                .collect(Collectors.toList());//最後將流轉爲集合
        return getDishNamesByStream;
    }


    public static void main(String[] args) {

        /** 模擬菜單**/
        List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 300, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH));

        List<String> dishNamesByCollections = getDishNamesByCollections(menu);
        System.out.println("集合處理方式:"+dishNamesByCollections);

        List<String> getDishNamesByStream = getDishNamesByStream(menu);
        System.out.println("Stream處理方式:"+getDishNamesByStream);

    }

}

 打印結果:

上面的需求實現類中,我們可以看到Collection和Stream兩個實現方式相比較下,Stream在直觀上最大的優點就是:代碼更加簡潔;而且Stream允許你聲明式的編碼,也就是從代碼上直觀展示出我們幹嘛幹嘛(我要篩選熱量<400的菜等等),而不再冗雜的循環和if條件;此外,上面代碼,我直接用到了parallelStream()方法,這個方法可以利用多核架構並行執行代碼,無論是並行處理的性能還是線程條數,都不需要你在考慮。

二、關於Stream API的一些必知概念及特性

1、什麼是流?

上面我們只是引出了Stream這個東西,並簡單對比了集合與流。那麼Stream 流 到底怎麼理解呢?

在《java in action》書中的定義是:"a sequence of elements from a source that supports data processing operations.",以我小學一級英語翻譯過來就是“支持數據處理操作的一個source(資源?) 中的元素序列”。這個定義似乎有點不太通俗,下面讓我們分解一下這個定義,以通俗的的理解什麼Stream,Stream的結構和組成。

Sequence of elements(元素序列):這個其實就是 source 裏面的東西。對應上面的菜單程序,它其實就Dish部分,即源碼中的黃色框框部分:

source(數據源) :Stream流的作用就是操作數據,那麼source 就是爲Stream提供可操作的源數據的。對應上面的菜單程序,它就是menu(下圖。一般,集合、數組或I/OI/O resources 都可以成爲Stream的source :

Data processing operations(數據處理操作):上面菜單程序代碼中出現的filter、sorted、map、collect,以及我們後來會用到的reduce、find、match等都屬於Stream 的一些操作數據的方法接口。這些操作可以順序進行,也可以並行執行。

Pipelining(管道、流水線):Stream對數據的操作類似數據庫查詢,也像電子廠的生產流線一樣,Stream的每一箇中間操作(後面解釋什麼是中間操作)比如上面的filter、sorted、map,每一步都會返回一個新的流,這些操作全部連起來就是想是一個工廠得生產流水線, like this:

Internal iteration(內部迭代):Stream API 實現了對數據迭代的封裝,不用你再像操作集合一樣,手動寫for循環顯示迭代數據。

2、Stream 流的使用。

java8 的 Stream API 提供了很多數據操作接口,這裏面一些常用接口其實我們在第一部分那個例子中就看到了,有基礎的朋友其實看了第一部分那個例子已經可以進行一些簡單的Stream操作。那麼現在我們詳細來看看Stream的一些基本操作和操作概念,讓我們不只停留在會用。

我們想看看我們最開始使用到的菜單程序源碼,我做了一些標註:

上面我們引出了兩個新的概念,中間操作,終端操作(其實我們上面也提到了),然後你可以看到兩類操作:

  • filter、sorted和map可以連成一條流水線。
  • collect 則觸發流水線執行,返回集合並關閉它。

3、什麼叫中間操作,中間操作的執行順序?

可以連接起來的Stream流操作,諸如filter或sorted等可以返回一個Stream 流的操作,就叫中間操作。只有存在一個終端操作,且觸發了終端操作,中間操作纔會開始執行。

現在我們爲菜單程序編寫一個新的方法,以便直觀看到中間操作的執行循序:

    
    //查找菜單中卡路里大於300的菜名 ,只需要輸出四個就行
    public static List<String> getDishNamesByStream(List<Dish> menu){

        List<String> getDishNamesByStream =
                menu.stream()
                        .filter(d -> {
                            System.out.println("執行filter—>" + d.getName());
                            return d.getCalories() > 300;
                        })
                        .map(d -> {
                            System.out.println("執行mapping—>" + d.getName());
                            return d.getName();
                        })
                        .limit(4)
                        .collect(toList());

        return getDishNamesByStream;
    }

執行結果:

我們通過控制檯的打印信息可以知道,雖然filter和map是兩個獨立的中間操作,但它們合併到同一次遍歷中執行,這就叫作循環合併。

4、什麼叫終端操作?

可以啓動中間操作和關閉中間流的操作稱爲終端操作,終端操作會從流的流水線(中間操作)生成結果。其結果是任何非Stream的值,比如可能是List、Integer,甚至void。例:在下面的流水線中,forEach作爲一個流的終端操作,它只是打印一數據,並沒有返回數據:

menu.stream().forEach(System.out::println);

5、 Stream 流的生命週期

同一個流只能遍歷一次,遍歷完後,這個流就已經被消費掉了。你如果還需要在遍歷,可以從原始數據源那裏再獲得一個新的流來重新遍歷一遍。

舉個例子:

同一個流 s 被兩次用於forEach的終端操作,此時控制檯不報錯:

但是你從原始數據源menu那裏再獲得一個新的流在操作就可以:

6、Stream的內部迭代與Collection的外部迭代

使用Collection需要你自己去做迭代(比如用for-each),這稱爲外部迭代。 但是Streams API提供了內部迭代,也就是說它幫你把迭代操作實現並封裝了,還把得到的流存在了某個地方。舉個例子:

集合:用for-each循環外部迭代:

List<String> names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}

集合:用背後的迭代器做外部迭代

List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}

流:內部迭代

List<String> names = menu.stream()
.map(Dish::getName)
.collect(toList());

是的,你沒看到迭代代碼

總的來說,Stream API 提供的內部迭代可以自動進行並行處理,或者用更優化的順序進行處理。相比Java過去集合用的那種外部迭代方法,一旦你寫for-each,選擇了外部迭代,那你基本上就要自己處理所有的並行問題了,這些優化都是很繁雜的。

 三、Stream 流的入門學習和使用小結

1、流的使用一般包括三件事(參照本文開頭的案例代碼):

  •  一個數據源(如集合)來執行一個查詢;
  • 一箇中間操作鏈,形成一條流的流水線;
  • 一個終端操作,執行流水線,並能生成結果。

2、Stream API提供的一些常用操作接口:

 

java8 新特性解析(更新中):

Java8新特性1:lambda表達式入門--由淺入深,從單發步槍邁向自動步槍 

Java8新特性2:方法引用--深入理解雙冒號::的使用

Java8新特性3:Stream1——什麼是Stream,Stream的特性,如何使用Stream,Stream與Collection集合的區別

Java8新特性3:Stream2—一文詳解Stream API,讓你快速理解Stream Api提供的諸多常用方法

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章