此流非彼流——Stream詳解

說故事的五公子發表於2021-01-20

Stream是什麼?

Java從8開始,不但引入了Lambda表示式,還引入了一個全新的流式API:Stream API。它位於java.util.stream包中。

Stream 使用一種類似用 SQL 語句從資料庫查詢資料的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。

Stream API可以極大提高Java程式設計師的生產力,讓程式設計師寫出高效率、乾淨、簡潔的程式碼。這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。元素流在管道中經過中間操作(intermediate operation)的處理,最後由最終操作(terminal operation)得到前面處理的結果。

Stream和IO包下的InputStream和OutputStream一樣嗎?

劃重點:這個Stream不同於java.ioInputStreamOutputStream,它代表的是任意Java物件的序列。兩者對比如下:

java.io java.util.stream
儲存 順序讀寫的bytechar 順序輸出的任意Java物件例項
用途 序列化至檔案或網路 記憶體計算/業務邏輯

這時候大家可能又有疑問了,那麼既然是順序輸出的任意Java物件例項,那麼和List集合不就相同了嗎?

再次劃重點:這個StreamList也不一樣,List儲存的每個元素都是已經儲存在記憶體中的某個Java物件,而Stream輸出的元素可能並沒有預先儲存在記憶體中,而是實時計算出來的。

換句話說,List的用途是操作一組已存在的Java物件,而Stream實現的是惰性計算,兩者對比如下:

java.util.List java.util.stream
元素 已分配並儲存在記憶體 可能未分配,實時計算
用途 操作一組已存在的Java物件 惰性計算

關於惰性計算在下面的章節中可以看到。

Stream特點

Stream介面還包含幾個基本型別的子介面如IntStream, LongStream 和 DoubleStream。

特點:

  • 不儲存資料:流是基於資料來源的物件,它本身不儲存資料元素,而是通過管道將資料來源的元素傳遞給操作。
  • 函數語言程式設計:流的操作不會修改資料來源,例如filter不會將資料來源中的資料刪除。
  • 延遲操作:流的很多操作如filter,map等中間操作是延遲執行的,只有到終點操作才會將操作順序執行。
  • 純消費:流的元素只能訪問一次,類似Iterator,操作沒有回頭路,如果你想從頭重新訪問流的元素,對不起,你得重新生成一個新的流。

Stream的建立

Stream的建立有多種方式,下面給大家一一列舉出來

1、Stream.of()

這種方式一般不常用的,但是測試的時候比較方便

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("1", "2", "3", "4");
        //forEach()方法相當於內部迴圈呼叫
        //引數的寫法是Lambda表示式
        stream.forEach(s -> System.out.println(s));
    }
}

關於Lambda表示式,在我的這篇部落格中有詳細介紹,感興趣的朋友可以去看一下

2、基於陣列或者Collection

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> stream1 = Arrays.stream(new String[] { "1", "2", "3" });
        Stream<String> stream2 = List.of("X", "Y", "Z").stream();
        stream1.forEach(System.out::println);
        stream2.forEach(System.out::println);
    }
}

這兩種建立Stream的方式是我們工作中經常會用到的方式,藉助Stream(轉化、聚合等方法)可以幫助我們更方便的去輸出我們想要的結果

3、其他方式

  • 使用流的靜態方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator),如Stream.iterate(0, n -> n * 2),或者generate(Supplier<T> s)Stream.generate(Math::random)

  • BufferedReader.lines()從檔案中獲得行的流。

  • Files類的操作路徑的方法,如listfindwalk等。

  • 隨機數流Random.ints()

  • 其它一些類提供了建立流的方法,如BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence), 和 JarFile.stream()

  • 更底層的使用StreamSupport,它提供了將Spliterator轉換成流的方法。

Stream常用API(中間操作)

還記得我們在前面介紹Stream的時候提到了一個惰性計算。惰性計算的特點是:一個Stream轉換為另一個Stream時,實際上只儲存了轉換規則,並沒有任何計算髮生。中間操作會返回一個新的流,它不會修改原始的資料來源,而且是由在終點操作開始的時候才真正開始執行。

1、distinct

distinct保證輸出的流中包含唯一的元素,它是通過Object.equals(Object)來檢查是否包含相同的元素。

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("a", "b", "c", "b","c","d").distinct();
        stream.forEach(System.out::println);
    }
}
//輸出結果
a
b
c
d

2、filter

從字面看是過濾的意思,過濾掉不滿足條件的資料

import java.util.stream.IntStream;

public class StreamTest {
    public static void main(String[] args) {
        IntStream stream = IntStream.range(1, 10).filter(i -> i % 2 == 0); //filter中的引數是過濾條件 
        stream.forEach(System.out::println);
    }
}
//輸出結果
2
4
6
8

3、map

map方法可以將流中的值對映成另外的值,比如將字串全部轉化成小寫

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Hello WORLD HELLO Life").map(s -> s.toLowerCase()); 
        stream.forEach(System.out::println);
    }
}
//輸出結果
hello world hello life

從輸出結果我們可以看到,字串全部轉化成小寫字元了

4、limit

limit方法指定流的元素數列,類似於Mysql中的limit方法

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("1", "2", "3", "4", "5", "6").limit(3); //取三條
        stream.forEach(System.out::println);
    }
}
// 輸出結果
1
2
3

5、peek

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Hello WORLD HELLO Life").peek(s -> {
            String peek = s.toLowerCase();
            System.out.println(peek);
        });
        stream.forEach(System.out::println);
    }
}
//輸出結果
hello world hello life
Hello WORLD HELLO Life

有沒有發現出一些東西?

我們將這段程式碼用上面的map方法實現一下

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Hello WORLD HELLO Life").map(s -> {
            String peek = s.toLowerCase();
            System.out.println(peek);
            return peek;
        });
        stream.forEach(System.out::println);
    }
}
// 輸出結果
hello world hello life
hello world hello life

peek方法的定義如下:

Stream<T> peek(Consumer<? super T> action);

peek方法接收一個Consumer的入參。瞭解λ表示式的應該明白 Consumer的實現類 應該只有一個方法,該方法返回型別為void。

而map方法的入參為 Function。

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

我們發現Function 比 Consumer 多了一個 return。這也就是peek 與 map的區別了。

6、skip

skip返回丟棄了前n個元素的流,如果流中的元素小於或者等於n,則返回空的流。

7、sorted

sorted()將流中的元素按照自然排序方式進行排序

import java.util.stream.Stream;

public class QueryTest {

    public static void main(String[] args) {

        //自定義排序
        customSort();
        //自然排序
        naturalSort();

    }

    public static void customSort() {
        Stream stream = Stream.of("hello", "I", "love", "you").sorted((str1, str2) -> {
            // 自定義排序規則
            if (str1 == null) {
                return -1;
            }
            if (str2 == null) {
                return 1;
            }
            return str1.length() - str2.length();
        });
        System.out.println("-----------自定義排序-----------");
        stream.forEach(System.out::println);
    }

    public static void naturalSort() {
        Stream<String> stream = Stream.of("hello", "I", "love", "you").sorted();
        System.out.println("-----------自然排序------------");
        stream.forEach(System.out::println);
    }

}

// 輸出結果
-----------自定義排序-----------
I
you
love
hello
-----------自然排序------------
I
hello
love
you

如果我們直接呼叫sorted()方法,那麼將按照自然排序,如果我們希望元素按照我們想要的結果來排序,需要自定義排序方法,sorted(Comparator<? super T> comparator)可以指定排序的方式。如果元素沒有實現Comparable,則終點操作執行時會丟擲java.lang.ClassCastException異常。

Stream常用API(終點操作)

1、max、min、count

max:獲取最大值

min:獲取最小值

count:返回流的數量

2、reduce

reduce操作可以實現從一組元素中生成一個值,max()min()count()等都是reduce操作,將他們單獨設為函式只是因為常用。reduce()的方法定義有三種重寫形式:

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

3、count

獲取Stream數量

package com.mybatisplus;

import java.util.stream.Stream;

public class QueryTest {

    public static void main(String[] args) {
        long count = Stream.of("a", "b", "A", "a", "c", "a").count();
        System.out.println(count);
    }

}

//輸出結果  6

4、Match

anyMatch表示,判斷的條件裡,任意一個元素成功,返回true

allMatch表示,判斷條件裡的元素,所有的都是,返回true

noneMatch跟allMatch相反,判斷條件裡的元素,所有的都不是,返回true

package com.mybatisplus;

import java.util.stream.Stream;

public class QueryTest {

    public static void main(String[] args) {
        boolean b1 = Stream.of("a", "b", "A", "a", "c", "a").anyMatch(str -> str.equals("a"));
        boolean b2 = Stream.of("a", "b", "A", "a", "c", "a").allMatch(str -> str.equals("a"));
        boolean b3 = Stream.of("a", "b", "A", "a", "c", "a").noneMatch(str -> str.equals("a"));

        System.out.println("b1 = " + b1);
        System.out.println("b2 = " + b2);
        System.out.println("b3 = " + b3);
    }

}
// 輸出結果
b1 = true
b2 = false
b3 = false

相關文章