Java8新特性之Stream流詳細總結

未經允許禁止轉載,轉載請聯繫作者。

目錄

一:什麼是 Stream

1.1 簡介

1.2 Stream API的特點:

二 Stream流的創建

2.1 通過Collection 接口函數

2.2 通過Stream

2.2.1通過Stream:由值創流

2.2.2 通過Stream:函數創流

三 Stream流的中間操作之常用用法

3.1 filter方法

3.2 concat方法

3.3 map方法

3.4 flatMap方法

3.5 Stream中的reduce方法

3.6 peek方法

3.7 limit/skip方法

3.8 sorted方法

3.9 distinct方法

3.10 一個小彙總實例

四 Stream最終操作

4.1 forEach方法

4.2 count方法

4.3 collect方法

4.4 findFirst/anyMatch/max/min等其他方法


一:什麼是 Stream

1.1 簡介

java8新添加了一個特性:流Stream。Stream和I/O流不同,它更像具有Iterable的集合類,但行爲和集合類又有所不同,它是對集合對象功能的增強,讓開發者能夠以一種聲明的方式處理數據源(集合、數組等),它專注於對數據源進行各種高效的聚合操作(aggregate operation)和大批量數據操作 (bulk data operation)。

舉個例子,只要給出需要對其包含的元素執行什麼操作,比如 “過濾掉長度大於 10 的字符串”、“獲取每個字符串的首字母”等,Stream 會隱式地在內部進行遍歷,做出相應的數據轉換。

Stream是一種對 Java 集合運算和表達的高階抽象。Stream API將處理的數據源看做一種Stream(流),Stream(流)在Pipeline(管道)中傳輸和運算,支持的運算包含篩選、排序、聚合等,當到達終點後便得到最終的處理結果。

幾個關鍵概念

  1. 元素 Stream是一個來自數據源的元素隊列,Stream本身並不存儲元素。
  2. 數據源(即Stream的來源)包含集合、數組、I/O channel、generator(發生器)等。
  3. 聚合操作 類似SQL中的filter、map、find、match、sorted等操作
  4. 管道運算 Stream在Pipeline中運算後返回Stream對象本身,這樣多個操作串聯成一個Pipeline,並形成fluent風格的代碼。這種方式可以優化操作,如延遲執行(laziness)和短路( short-circuiting)。
  5. 內部迭代 不同於java8以前對集合的遍歷方式(外部迭代),Stream API採用訪問者模式(Visitor)實現了內部迭代。
  6. 並行運算 Stream API支持串行(stream() )或並行(parallelStream() )的兩種操作方式。

1.2 Stream API的特點:

  1. Stream API的使用和同樣是java8新特性的lambda表達式密不可分,可以大大提高編碼效率和代碼可讀性。
  2. Stream API提供串行和並行兩種操作,其中並行操作能發揮多核處理器的優勢,使用fork/join的方式進行並行操作以提高運行速度。
  3. Stream API進行並行操作無需編寫多線程代碼即可寫出高效的併發程序,且通常可避免多線程代碼出錯的問題。

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

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

爲什麼選擇Stream呢?體驗函數式編程帶來的便捷!減少你的代碼!

Stream的操作是建立在函數式接口的組合上的,函數式接口,對於Java來說就是接口內只有一個公開方法的接口。Java8提供了很多函數式接口,一般都使用註解@FunctionalInterface聲明,有必要一些函數式接口。


二 Stream流的創建

stream的類圖關係:

 

2.1 通過Collection 接口函數

Java8 中的 Collection 接口被擴展, 供了兩個獲取流的方法:
1. default Stream< E> stream() : 返回一個順序流

List<String> strings = Arrays.asList("test", "lucas", "Hello", "HelloWorld", "武漢加油");
Stream<String> stream = strings.stream();

2. default Stream< E> parallelStream() : 返回一個並行流

 List<Employee> list = new ArrayList<>();
 Stream<Employee> stream = list.stream();
 Stream<Employee> parallelStream = list.parallelStream();

並行流是多線程方式,需要考慮線程安全問題。

2.2 通過Stream

2.2.1通過Stream:由值創流

Stream<String> stream = Stream.of("test", "lucas", "Hello", "HelloWorld", "武漢加油");

2.2.2 通過Stream:函數創流

可以使用Stream的靜態方法 Stream.iterate() Stream.generate(), 創建無限流。
1. 迭代:public static< T> Stream< T> iterate(final T seed, final UnaryOperator< T> f)
2. 生成:public static< T> Stream< T> generate(Supplier< T> s)

例子:

        // 迭代
        Stream<Integer> stream3 = Stream.iterate(0, (x) -> x + 2).limit(2);
        stream3.forEach(System.out::println);

        System.out.println("-------------");

        // 生成
        Stream<Double> generateA = Stream.generate(new Supplier<Double>() {
            @Override
            public Double get() {
                return java.lang.Math.random();
            }
        });
        Stream<Double> generateB = Stream.generate(()-> java.lang.Math.random());
        Stream<Double> generateC = Stream.generate(java.lang.Math::random).limit(4);
        generateC.forEach(System.out::println);

//執行結果
0
2
-------------
0.0064617087705397536
0.24943325913078163
0.9396182936441738
0.031970039813425166

這種函數創建方法較少使用,但也因場景而異。上例可以直觀理解並加深印象:Stream流和Lambda表達式的結合多麼的重要!代碼因此簡潔易懂。


三 Stream流的中間操作之常用用法

Stream有很多中間操作,多箇中間操作可以連接起來形成一個流水線,每一箇中間操作就像流水線上的一個工人,每人工人都可以對流進行加工,加工後得到的結果還是一個流,除非流水線上觸發終止操作,否則中間操作不會執行任何的處理! 而在終止操作時一次性全部處理,稱爲“惰性求值”。

這些中間操作就是我們經常用到的一些Stream的接口用法:

3.1 filter方法

filter 方法通過設置條件對元素進行過濾並得到一個新流(就是你去選擇過濾流中的元素)。

例子,以下代碼片段使用 filter 方法過濾掉空字符串:

    public static void main(String[] args) {
        List<String> strings = Arrays.asList("test", "lucas", "Hello", "HelloWorld", "武漢加油");
        strings.stream().filter(string -> !string.isEmpty()).forEach(System.out::println);
    }

運行結果

test
lucas
Hello
HelloWorld
武漢加油

3.2 concat方法

concat方法將兩個Stream連接在一起,合成一個Stream。若兩個輸入的Stream都時排序的,則新Stream也是排序的;若輸入的Stream中任何一個是並行的,則新的Stream也是並行的;若關閉新的Stream時,原兩個輸入的Stream都將執行關閉處理。

例子:

        Stream.concat(Stream.of(1, 4, 3), Stream.of(2, 5))
                .forEach(integer -> System.out.print(integer + "  "));

運行結果

1  4  3  2  5 

3.3 map方法

方法 描述
map(Function f) 接收一個函數作爲參數,該函數會被應用到每個元素上,並將其映射成一個新的元素。
mapToDouble(ToDoubleFunction f) 接收一個函數作爲參數,該函數會被應用到每個元素上,產生一個新的 DoubleStream。
mapToInt(ToIntFunction f) 接收一個函數作爲參數,該函數會被應用到每個元素上,產生一個新的 IntStream。
mapToLong(ToLongFunction f) 接收一個函數作爲參數,該函數會被應用到每個元素上,產生一個新的 LongStream。
flatMap(Function f) 接收一個函數作爲參數,將流中的每個值都換成另一個流,然後把所有流連接成一個流

map 方法用於映射每個元素到對應的結果。

例子,以下代碼片段使用 map 輸出了元素對應的平方數:

        List<String> strings = Arrays.asList("test", "lucas", "Hello","HelloWorld", "武漢加油");
        strings.stream()
                .filter(string -> !string.isEmpty())
                .map(String::length)
                .forEach(System.out::println);

//運行結果 
4
5
5
10
4

我們略看下map的源碼:

這邊需要強調的一點是,Function是java8新增的函數式接口,map需要傳入一個Function的實現,這邊又直接利用Lambda表達式完美地讓我們只去關注map()括號中的函數而省略了很多繁雜的接口實現的書寫(比如上例的String::length,看到“::"不要疑惑,Java 8的另一個特點“引用方法”就是用的冒號“::”來進行方法的調用)。
 

3.4 flatMap方法

flatMap方法與map方法類似,map方法會再創建一個新的Stream,而flatmap()是將原Stream的元素取代爲轉換的Stream。

如果轉換函數生產的Stream爲null,應由空Stream取代。

flatMap有三個對於原始類型的變種方法,分別是:flatMapToInt,flatMapToLong和flatMapToDouble。

Stream.of(1, 2, 3)
    .flatMap(integer -> Stream.of(integer * 10))
    .forEach(System.out::println);
    // 10,20,30

傳給flatMap中的表達式接受了一個Integer類型的參數,通過轉換函數,將原元素乘以10後,生成一個只有該元素的流,該流取代原流中的元素。

3.5 Stream中的reduce方法

reduce方法有三個重載的方法。

第一個方法

接受一個BinaryOperator類型的lambada表達式, 常規應用方法如下

Optional<T> reduce(BinaryOperator<T> accumulator);

例子

List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce((a,b) -> a + b ).get();
System.out.println(result);

第二個方法

與第一個方法的實現的唯一區別是它首次執行時表達式第一次參數並不是stream的第一個元素,而是通過簽名的第一個參數identity來指定。

T reduce(T identity, BinaryOperator<T> accumulator);

例子

List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce(0,(a,b) ->  a + b );
System.out.println(result);

其實這兩種實現幾乎差別,第一種比第一種僅僅多了一個字定義初始值罷了。 此外,因爲存在stream爲空的情況,所以第一種實現並不直接方法計算的結果,而是將計算結果用Optional來包裝,我們可以通過它的get方法獲得一個Integer類型的結果,而Integer允許null。第二種實現因爲允許指定初始值,因此即使stream爲空,也不會出現返回結果爲null的情況,當stream爲空,reduce爲直接把初始值返回。

第三個方法

第三種方法的用法相較前兩種稍顯複雜,由於前兩種實現有一個缺陷,它們的計算結果必須和stream中的元素類型相同,如上面的代碼示例,stream中的類型爲int,那麼計算結果也必須爲int,這導致了靈活性的不足,甚至無法完成某些任務, 比入我們咬對一個一系列int值求和,但是求和的結果用一個int類型已經放不下,必須升級爲long類型,此實第三簽名就能發揮價值了,它不將執行結果與stream中元素的類型綁死。

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

例子

List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6);
        ArrayList<String> result = numList.stream().reduce(new ArrayList<String>(), (a, b) -> {
            a.add("int轉成string:" + Integer.toString(b));
            return a;
        }, (a, b) -> null);
        System.out.println(result);
//[int轉成string:1, int轉成string:2, int轉成string:3, int轉成string:4, int轉成string:5, int轉成string:6]

如果你使用了parallelStream reduce操作是併發進行的,爲了避免競爭,每個reduce線程都會有獨立的result combiner的作用在於合併每個線程的result得到最終結果。

BinaryOperator是供多線程使用的,如果不在Stream中聲明使用多線程,就不會使用子任務,自然也不會調用到該方法。另外多線程下使用BinaryOperator的時候是需要考慮線程安全的問題。

3.6 peek方法

peek方法生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函數(Consumer的實例),新Stream每個元素被消費的時候都會優先執行給定的消費函數,具體可看Consumer函數。

        Stream.of(1, 2, 3)
                .peek(integer -> System.out.println("peek中優先執行的消費函數:這是" + integer))
                .forEach(System.out::println);

運行結果:

peek中優先執行的消費函數:這是1
1
peek中優先執行的消費函數:這是2
2
peek中優先執行的消費函數:這是3
3

3.7 limit/skip方法

limit 返回 Stream 的前面 n 個元素;

skip 則是扔掉前 n 個元素。

例子,以下代碼片段使用 limit 方法保理4個元素:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().limit(4).forEach(System.out::println);
//1,2,3,4

3.8 sorted方法

sorted 方法用於對流進行排序。

例子,以下代碼片段使用 sorted 方法進行排序:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
numbers.stream().sorted().forEach(System.out::println);
//2,2,3,3,3,5,7

3.9 distinct方法

distinct主要用來去重。

例子,以下代碼片段使用 distinct 對元素進行去重:

List<Integer> numbers = Arrays.asList(3, 3, 7, 3, 5);
numbers.stream().distinct().forEach(System.out::println);
//3,7,5

3.10 一個小彙總實例

        List<String> strings = Arrays.asList("", "lucas", "Hello", "HelloWorld", "武漢加油");
        Stream<Integer> distinct = strings.stream()
                .filter(string -> string.length() <= 6) //過濾掉了"HelloWorld"
                .map(String::length)  //將Stream從原來元數據的String類型變成了int
                .sorted() //int從小到大
                .limit(2) //只選前兩個
                .distinct(); //去重
       distinct.forEach( System.out::println);

運行結果:

0
4

四 Stream最終操作

最終操作(terminal operation)解釋:終止操作會從流的流水線生成結果。其結果可以是任何不是流的值,例如:List、Integer等。而Stream的中間操作得到的結果還是一個Stream,那麼如何把一個Stream轉換成我們需要的類型呢?這就需要最終操作(terminal operation)

最終操作會消耗流,產生一個最終結果。也就是說,在最終操作之後,不能再次使用流,也不能在使用任何中間操作,否則將拋出異常:

java.lang.IllegalStateException: stream has already been operated upon or closed
複製代碼

常用的最終操作如下:

方法 描述
allMatch(Predicate p) 檢查是否匹配所有元素
anyMatch(Predicate p) 檢查是否至少匹配一個元素
noneMatch(Predicate p) 檢查是否沒有匹配所有元素
findFirst() 返回第一個元素
findAny() 返回當前流中的任意元素
count() 返回流中元素總數
max(Comparator c) 返回流中最大值
min(Comparator c) 返回流中最小值
forEach(Consumer c) 內部迭代(使用 Collection 接口需要用戶去做迭代,稱爲外部迭代。相反,Stream API 使用內部 迭代——它幫你把迭代做了)

4.1 forEach方法

Stream 提供了方法 'forEach' 來迭代流中的每個數據。注意forEach方法的參數是Cousumer。

以下代碼片段使用 forEach 輸出了10個隨機數:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

4.2 count方法

count用來統計流中的元素個數。

        List<String> strings = Arrays.asList("", "lucas", "Hello", "HelloWorld", "武漢加油");

        long count = strings.stream()
                .filter(string -> string.length() <= 6)
                .map(String::length)
                .sorted()
                .limit(2)
                .distinct()
                .count();
        System.out.println(count);
      
//2

4.3 collect方法

collect中有很多的靜態方法,靈活運用會起到四兩撥千斤的作用。

collect是一個歸約操作,可以接受各種做法作爲參數,將流中的元素累積成一個彙總結果:


        List<String> strings = Arrays.asList("", "lucas", "Hello", "HelloWorld", "武漢加油");

        List<Integer> collect = strings
                .stream()
                .filter(string -> string.length() <= 6)
                .map(String::length)
                .sorted()
                .limit(2)
                .distinct()
                .collect(Collectors.toList());
        collect.forEach(System.out::println);

//運行結果
0
4

如上,最終得到一個List 數組,也就是流最終的歸宿。

以上只屬於collect一個簡單生成List例子,Collect有更多便捷的操作供開發者選擇使用,我們可以利用Collectors類中提供的各種靜態方法,組合實現很多多樣化的需求。

Collectors類中提供的一些常用靜態方法:

方法 返回類型 作用 實例
toList List<T> 把流中元素收集到List List<Employee> emps= list.stream().collect(Collectors.toList());
toSet Set<T> 把流中元素收集到Set Set<Employee> emps= list.stream().collect(Collectors.toSet());
toCollection Collection<T> 把流中元素收集到創建的集合 Collection<Employee>emps=list.stream().collect(Collectors.toCollection(ArrayList::new));
counting Long 計算流中元素的個數 long count = list.stream().collect(Collectors.counting());
summingInt Integer 對流中元素的整數屬性求和 inttotal=list.stream().collect(Collectors.summingInt(Employee::getSalary));
joining String 連接流中每個字符串 String str= list.stream().map(Employee::getName).collect(Collectors.joining());
groupingBy Map<K, List<T>> 根據某屬性值對流分組,屬 性爲K,結果爲V Map<Emp.Status, List<Emp>> map= list.stream() .collect(Collectors.groupingBy(Employee::getStatus));
partitioningBy Map<Boolean, List<T>> 根據true或false進行分區 Map<Boolean,List<Emp>>vd= list.stream().collect(Collectors.partitioningBy(Employee::getManage));
reducing 歸約產生的類型 從一個作爲累加器的初始值 開始,利用BinaryOperator與 流中元素逐個結合,從而歸約成單個值 inttotal=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));
averagingInt Double 計算流中元素Integer屬性的平均值 doubleavg= list.stream().collect(Collectors.averagingInt(Employee::getSalary));
maxBy Optional<T> 根據比較器選擇最大值 Optional<Emp>max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));
minBy Optional<T> 根據比較器選擇最小值 Optional<Emp> min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
summarizingInt IntSummaryStatistics 收集流中Integer屬性的統計值。 如:平均值 IntSummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));
collectingAndThen 轉換函數返回的類型 包裹另一個收集器,對其結 果轉換函數 inthow= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));

下面舉一個分組和多級分組(其實就是嵌套使用groupingBy)的例子:

        List<Person> person_list = Arrays.asList(
                new Person("lucas", 26),
                new Person("lucas", 25),
                new Person("lucas", 60),
                new Person( "chris", 27),
                new Person( "chris", 20),
                new Person("theo", 52)
        );

        System.out.println("-------- 利用Collectors的靜態方法一級分組---------");
        Map<String, List<Person>> group = person_list.stream()
                .collect(Collectors.groupingBy(Person::getName));
        System.out.println(group);

        System.out.println("-------- 利用Collectors的靜態方法二級分組---------");
        Map<String, Map<String, List<Person>>> group2 = person_list.stream()
                .collect(Collectors.groupingBy(Person::getName, Collectors.groupingBy((people) -> {
                    if (people.getAge() < 30) return "青年";
                    else if (people.getAge() < 50) return "中年";
                    else return "老年";
                })));
        System.out.println(group);

運行結果:

-------- 利用Collectors的靜態方法一級分組---------
{
chris=[cn.lucas.Person@52cc8049, cn.lucas.Person@5b6f7412], 
lucas=[cn.lucas.Person@27973e9b, cn.lucas.Person@312b1dae, cn.lucas.Person@7530d0a], 
theo=[cn.lucas.Person@27bc2616]
}
-------- 利用Collectors的靜態方法二級分組---------
{
chris={
    青年=[cn.lucas.Person@52cc8049, cn.lucas.Person@5b6f7412]
      }, 
lucas={
    青年=[cn.lucas.Person@27973e9b, cn.lucas.Person@312b1dae], 
    老年=[cn.lucas.Person@7530d0a]
      }, 
theo={
    老年=[cn.lucas.Person@27bc2616]
     }
}

可以看到,巧妙地利用Collect裏的方法,對於一些“髒活兒累活兒乾死了又沒有太大價值的活兒”而言,實現起來是多麼的方便優雅(上述邏輯直接可以用到一堆數據二級列表展示時的List數據處理過程中)!我們主需要把關注點,並且主要的代碼量也都在代碼的業務邏輯上了。

再來一個Collectors.partitioningBy分割數據塊的例子,會讓你感到簡單又優雅:

    public static void main(String[] args) {

        Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
                .collect(Collectors.partitioningBy(it -> it % 2 == 0));
        System.out.println("分割數據塊 : " + collectParti);

    }
    //分割數據塊 : {false=[1, 3], true=[2, 4]}

4.4 findFirst/anyMatch/max/min等其他方法

        List<Person> person_list = Arrays.asList(
                new Person("lucas", 26),
                new Person("maggie", 25),
                new Person("queen", 23),
                new Person( "chris", 27),
                new Person( "max", 29),
                new Person("theo", 30)
        );


        System.out.println("-------- allMatch() 檢查是否匹配所有元素---------");
        boolean allMatch = person_list.stream()
                .allMatch((person -> person.getName().equals("lucas")));
        System.out.println(allMatch);

        System.out.println("-------- anyMatch() 檢查是否至少匹配一個元素---------");
        boolean anyMatch = person_list.stream()
                .anyMatch(person -> person.getName().equals("lucas"));
        System.out.println(anyMatch);

        System.out.println("-------- noneMatch  檢查是否沒有匹配所有元素---------");
        boolean noneMatch = person_list.stream()
                .noneMatch(person -> person.getName().equals("lucas"));
        System.out.println(noneMatch);

        System.out.println("-------- findFirst()  返回第一個元素---------");
        Optional<String> first = person_list.stream()
                .map(Person::getName)
                .findFirst(); // 獲取第一個元素
        System.out.println(first.get());

        System.out.println("-------- findAny()  返回當前流中的任意元素---------");
        boolean anyMatch1 = person_list.stream()
                .anyMatch(person -> person.getName().equals("lucas"));
        System.out.println(anyMatch1);

        System.out.println("-------- max()  返回流中最大值---------");
        Optional<Integer> intOptional = person_list.stream()
                .map(Person::getAge)
                .max(Integer::compare); //最大值
        System.out.println(intOptional.get());

運行結果:

-------- allMatch() 檢查是否匹配所有元素---------
false
-------- anyMatch() 檢查是否至少匹配一個元素---------
true
-------- noneMatch  檢查是否沒有匹配所有元素---------
false
-------- findFirst()  返回第一個元素---------
lucas
-------- findAny()  返回當前流中的任意元素---------
true
-------- max()  返回流中最大值---------
30

 

 

參考

https://juejin.im/post/5df4a93e51882512454b37fa

https://www.jianshu.com/p/2b40fd0765c3

https://blog.csdn.net/liudongdong0909/article/details/77429875

https://www.jianshu.com/p/e6cdc48bb355

https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

https://www.jianshu.com/p/71bee9a620b5

https://blog.csdn.net/u012706811/article/details/77096257

https://www.fulinlin.com/2019/07/25/yuque/Java8%E6%96%B0%E7%89%B9%E6%80%A7%20%20Stream%20%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B/#Filter

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