JUC併發編程學習(十三)-學習Stream流的使用

說到Stream便容易想到I/O Stream,而實際上,誰規定“流”就一定是“IO流”呢?在Java 8中,得益於Lambda所帶來的函數式編程,引入了一個全新的Stream概念,用於解決已有集合類庫既有的弊端。

傳統集合的多步遍歷代碼

幾乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或間接的遍歷操作。而當我們需要對集合中的元素進行操作的時候,除了必需的添加、刪除、獲取外,最典型的就是集合遍歷。例如:

import java.util.ArrayList;
import java.util.List;
public class Demo01ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強");
        list.add("張三丰");

        for (String name : list) {
        System.out.println(name);
        }
    }
}

這是一段非常簡單的集合遍歷操作:對集合中的每一個字符串都進行打印輸出操作。

循環遍歷的弊端

Java 8的Lambda讓我們可以更加專注於做什麼(What),而不是怎麼做(How),這點此前已經結合內部類進行了對比說明。現在,我們仔細體會一下上例代碼,可以發現:

  • for循環的語法就是“怎麼做”
  • for循環的循環體纔是“做什麼”

爲什麼使用循環?因爲要進行遍歷。但循環是遍歷的唯一方式嗎?遍歷是指每一個元素逐一進行處理,而並不是從第一個到最後一個順次處理的循環。前者是目的,後者是方式。
試想一下,如果希望對集合中的元素進行篩選過濾:

  1. 將集合A根據條件一過濾爲子集B;
  2. 然後再根據條件二過濾爲子集C。

那怎麼辦?在Java 8之前的做法可能爲:

import java.util.ArrayList;
import java.util.List;
public class Demo02NormalFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強");
        list.add("張三丰");

        List<String> zhangList = new ArrayList<>();
        for (String name : list) {
            if (name.startsWith("張")) {
            zhangList.add(name);
            }
        }

        List<String> shortList = new ArrayList<>();
        for (String name : zhangList) {
        if (name.length() == 3) {
            shortList.add(name);
            }
        }

        for (String name : shortList) {
            System.out.println(name);
        }
       }
}

這段代碼中含有三個循環,每一個作用不同:

  1. 首先篩選所有姓張的人;
  2. 然後篩選名字有三個字的人;
  3. 最後進行對結果進行打印輸出

每當我們需要對集合中的元素進行操作的時候,總是需要進行循環、循環、再循環。這是理所當然的麼?不是。循環是做事情的方式,而不是目的。另一方面,使用線性循環就意味着只能遍歷一次。如果希望再次遍歷,只能再使用另一個循環從頭開始。

那,Lambda的衍生物Stream能給我們帶來怎樣更加優雅的寫法呢?

Stream的更優寫法

package com.demo.java.study8;


import java.util.ArrayList;
import java.util.List;


/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {


    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("張無忌");
        list.add("周芷若");
        list.add("趙敏");
        list.add("張強");
        list.add("張三丰");
        list.add("張三sasa");


        list.stream().filter(s -> s.startsWith("張"))
                .filter(s -> s.length()==3)
                    .forEach(System.out::println);

    }
}

直接閱讀代碼的字面意思即可完美展示無關邏輯方式的語義:獲取流、過濾姓張、過濾長度爲3、逐一打印。代碼中並沒有體現使用線性循環或是其他任何算法進行遍歷,我們真正要做的事情內容被更好地體現在代碼中。

整體來看,流式思想類似於工廠車間的“生產流水線”。

什麼是 Stream?

Stream是操作集合的一種計算數據的工具。集合就是數據,Stream是計算集合中的數據的工具

和以前的Collection操作不同, Stream操作還有兩個基礎的特徵:

  • Pipelining: 中間操作都會返回流對象本身。 這樣多個操作可以串聯成一個管道, 如同流式風格(flfluentstyle)。 這樣做可以對操作進行優化, 比如延遲執行(laziness)和短路( short-circuiting)。
  • 內部迭代: 以前對集合遍歷都是通過Iterator或者增強for的方式, 顯式的在集合外部進行迭代, 這叫做外部迭代。 Stream提供了內部迭代的方式,流可以直接調用遍歷方法。

流的操作分爲兩種,分別爲中間操作 和 終端操作。

在這裏插入圖片描述

當使用一個流的時候,通常包括三個基本步驟:
獲取一個數據源(source)→ 數據轉換→執行操作獲取想要的結果。
每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。

獲取流

java.util.stream.Stream 是Java 8新加入的最常用的流接口。(這並不是一個函數式接口。)獲取一個流非常簡單,有以下幾種常用的方式:

  • 所有的 Collection 集合都可以通過 stream 默認方法獲取流;
  • Stream 接口的靜態方法 of 可以獲取數組對應的流

根據Collection獲取流
首先, java.util.Collection 接口中加入了default方法 stream 用來獲取流,所以其所有實現類均可獲取流

package com.demo.java.study8;

import java.util.*;
import java.util.stream.Stream;

/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {

    public static void main(String[] args) {
       List<String> list=new ArrayList<>();
        Stream stream=list.stream();


        Set<String> list2=new HashSet<>();
        Stream stream1=list2.stream();


        Vector<String> list3=new Vector<>();
        Stream stream2=list3.stream();
    }
}

根據Map獲取流
java.util.Map 接口不是 Collection 的子接口,且其K-V數據結構不符合流元素的單一特徵,所以獲取對應的流需要分key、value或entry等情況

package com.demo.java.study8;


import java.util.*;
import java.util.stream.Stream;


/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        Stream keyStream=map.keySet().stream();
        Stream valueStream=map.values().stream();
        Stream<Map.Entry<String,String>>    entryStream=map.entrySet().stream();
    }
}

根據數組獲取流

package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;

/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
    public static void main(String[] args) {
        String[] array = { "張無忌", "張翠山", "張三丰", "張一元" };
        Stream<String> stream=Stream.of(array);
    }
}

備註: of 方法的參數其實是一個可變參數,所以支持數組。

常用方法

流模型的操作很豐富,這裏介紹一些常用的API。這些方法可以被分成兩種:
延遲方法:返回值類型仍然是 Stream 接口自身類型的方法,因此支持鏈式調用。(除了終結方法外,其餘方法均爲延遲方法。)
終結方法:返回值類型不再是 Stream 接口自身類型的方法,因此不再支持類似 StringBuilder 那樣的鏈式調用。本小節中,終結方法包括 count 和 forEach 方法。

備註:本小節之外的更多方法,請自行參考API文檔。

逐一處理:forEach
雖然方法名字叫 forEach ,但是與for循環中的“for-each”暱稱不同。

void forEach(Consumer<? super T> action);

該方法接收一個 Consumer 接口函數,會將每一個流元素交給該函數進行處理。
複習Consumer接口

java.util.function.Consumer<T>接口是一個消費型接口。
Consumer接口中包含抽象方法void accept(T t),意爲消費一個指定泛型的數據。

基本使用

package com.demo.java.study8;


import java.util.*;
import java.util.stream.Stream;

public class Test {

    public static void main(String[] args) {
        String[] array = { "張無忌", "張翠山", "張三丰", "張一元" };
        Stream<String> stream=Stream.of(array);
        stream.forEach(s -> System.out.println(s));
    }
}

過濾:filter
可以通過 filter 方法將一個流轉換成另一個子集流。方法簽名:

Stream<T> filter(Predicate<? super T> predicate);

該接口接收一個 Predicate 函數式接口參數(可以是一個Lambda或方法引用)作爲篩選條件。
複習Predicate接口
此前我們已經學習過 java.util.stream.Predicate 函數式接口,其中唯一的抽象方法爲:

boolean test(T t);

該方法將會產生一個boolean值結果,代表指定的條件是否滿足。如果結果爲true,那麼Stream流的 filter 方法將會留用元素;如果結果爲false,那麼 filter 方法將會捨棄元素。

package com.demo.java.study8;


import java.util.*;
import java.util.stream.Stream;


/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {


    public static void main(String[] args) {


        String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
        Stream<String> stream=Stream.of(array);
        Stream<String> result= stream.filter(s -> s.startsWith("張"));
        result.forEach(s -> System.out.println(s));
    }
}

在這裏通過Lambda表達式來指定了篩選的條件:必須姓張。

映射:map
如果需要將流中的元素映射到另一個流中,可以使用 map 方法。方法簽名:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

該接口需要一個 Function 函數式接口參數,可以將當前流中的T類型數據轉換爲另一種R類型的流

複習Function接口
此前我們已經學習過 java.util.stream.Function 函數式接口,其中唯一的抽象方法爲:

R apply(T t);

這可以將一種T類型轉換成爲R類型,而這種轉換的動作,就稱爲“映射”。
基本使用

package com.demo.java.study8;


import java.util.stream.Stream;


/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-17 14:05
* @description:
* @Version: 1.0
*/
public class Test2 {


    public static void main(String[] args) {
        String[] array = { "10", "18", "20", "21" };
        Stream<String> stream=Stream.of(array);
         Stream result=  stream.map(s -> Integer.parseInt(s));
         result.forEach(s-> System.out.println(s));
    }
}

這段代碼中, map 方法的參數通過方法引用,將字符串類型轉換成爲了int類型(並自動裝箱爲 Integer 類對象)。

統計個數:count
正如舊集合 Collection 當中的 size 方法一樣,流提供 count 方法來數一數其中的元素個數:

long count()

該方法返回一個long值代表元素個數(不再像舊集合那樣是int值)。基本使用:

package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;

/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
    public static void main(String[] args) {
        String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
        Stream<String> stream=Stream.of(array);
        Stream<String> result= stream.filter(s -> s.startsWith("張"));
        System.out.println(result.count());
        
    }
}

取用前幾個:limit
limit 方法可以對流進行截取,只取用前n個。方法簽名:

Stream<T> limit(long maxSize);

參數是一個long型,如果集合當前長度大於參數則進行截取;否則不進行操作。基本使用:

package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;

/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
    public static void main(String[] args) {
        String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
        Stream<String> stream=Stream.of(array);
        Stream<String> result= stream.limit(3);
        result.forEach(s -> System.out.println(s));
        
    }
}

輸出:

張無忌
李翠山
劉三豐

跳過前幾個:skip
如果希望跳過前幾個元素,可以使用 skip 方法獲取一個截取之後的新流:

Stream<T> skip(long n);

如果流的當前長度大於n,則跳過前n個;否則將會得到一個長度爲0的空流。基本使用:

package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;

/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
    public static void main(String[] args) {
        String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
        Stream<String> stream=Stream.of(array);
        Stream<String> result= stream.skip(2);
        result.forEach(s -> System.out.println(s));
    }
}

輸出:

劉三豐
張一元

組合:concat
如果有兩個流,希望合併成爲一個流,那麼可以使用 Stream 接口的靜態方法 concat :

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

備註:這是一個靜態方法,與 java.lang.String 當中的 concat 方法是不同的。

package com.demo.java.study8;
import java.util.*;
import java.util.stream.Stream;
/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {
    public static void main(String[] args) {

        String[] array = { "張無忌", "李翠山", "劉三豐", "張一元" };
        Stream<String> stream1=Stream.of(array);
        Stream<String> stream2=Stream.of(array);

        Stream<String> result1= stream1.skip(2);
        Stream<String> result2=stream2.limit(2);
        Stream stream=Stream.concat(result1,result2);
        stream.forEach(s-> System.out.println(s));

    }
}

輸出:

劉三豐
張一元
張無忌
李翠山

練習:集合元素處理

題目

現在有兩個 ArrayList 集合存儲隊伍當中的多個成員姓名,要求使用Stream流式處理方式依次進行以下若干操作步驟:

  1. 第一個隊伍只要名字爲3個字的成員姓名;存儲到一個新集合中。
  2. 第一個隊伍篩選之後只要前3個人;存儲到一個新集合中。
  3. 第二個隊伍只要姓張的成員姓名;存儲到一個新集合中。
  4. 第二個隊伍篩選之後不要前2個人;存儲到一個新集合中。
  5. 將兩個隊伍合併爲一個隊伍;存儲到一個新集合中。
  6. 根據姓名創建 Person 對象;存儲到一個新集合中。
  7. 打印整個隊伍的Person對象信息。

兩個隊伍(集合)的代碼如下:

package com.demo.java.study8;


import java.util.*;
import java.util.stream.Stream;


/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {


    public static void main(String[] args) {


        //第一支隊伍
        ArrayList<String> one = new ArrayList<>();
        one.add("迪麗熱巴");
        one.add("宋遠橋");
        one.add("蘇星河");
        one.add("石破天");
        one.add("石中玉");
        one.add("老子");
        one.add("莊子");
        one.add("洪七公");


        //第二支隊伍
        ArrayList<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("張無忌");
        two.add("趙麗穎");
        two.add("張三丰");
        two.add("尼古拉斯趙四");
        two.add("張天愛");
        two.add("張二狗");
    }
}

解決:

package com.demo.java.study8;


import java.util.*;
import java.util.stream.Stream;


/**
* @ClassName:
* @PackageName: com.demo.java.study8
* @author: youjp
* @create: 2020-02-16 15:48
* @description:
* @Version: 1.0
*/
public class Test {


    public static void main(String[] args) {


        //第一支隊伍
        ArrayList<String> one = new ArrayList<>();
        one.add("迪麗熱巴");
        one.add("宋遠橋");
        one.add("蘇星河");
        one.add("石破天");
        one.add("石中玉");
        one.add("老子");
        one.add("莊子");
        one.add("洪七公");


        //第二支隊伍
        ArrayList<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("張無忌");
        two.add("趙麗穎");
        two.add("張三丰");
        two.add("尼古拉斯趙四");
        two.add("張天愛");
        two.add("張二狗");


        // 第一個隊伍只要名字爲3個字的成員姓名;
        // 第一個隊伍篩選之後只要前3個人;
        Stream<String> stream1=one.stream().filter(s -> s.length()==3).limit(3);


        // 第二個隊伍只要姓張的成員姓名;
        // 第二個隊伍篩選之後不要前2個人;
        Stream<String> stream2= two.stream().filter(s -> s.startsWith("張")).skip(2);

        // 將兩個隊伍合併爲一個隊伍;
        // 根據姓名創建Person對象;
        // 打印整個隊伍的Person對象信息。
        Stream<String> stream3=Stream.concat(stream1,stream2);
        stream3.map(Person::new).forEach(person -> System.out.println(person.toString()));
        
                //stream3.map(Person::new).forEach(System.out::println);

    }
}

輸出:

Person{name='宋遠橋'}
Person{name='蘇星河'}
Person{name='石破天'}
Person{name='張天愛'}
Person{name='張二狗'}

其他方法可另行參考博客:
https://blog.csdn.net/makyan/article/details/104533966

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