Java8 新特性 —— Stream 流式程式設計

for you is love發表於2020-11-15

集合優化了物件的儲存,大多數情況下,我們將物件儲存在集合是為了處理他們。使用流可以幫助我們處理物件,無需迭代集合中的元素,即可直接提取和操作元素,並新增了很多便利的操作,例如查詢、過濾、分組、排序等一系列操作。

流的一個核心好處是:它使得程式更加短小並且易於理解,當結合 Lambda 表示式和方法引用時,會讓人感覺自成一體。總而言之,流就是一種高效且易於使用的處理資料的方式。

觀察下面的例子:

public class Randoms {

public static void main(String[] args) {
    new Random(47)	// 建立 Random 物件,並給一個種子
        .ints(5, 20)	// 產生一個限定了邊界的隨機整數流
        .distinct()	// 使流中的整數不重複
        .limit(7)	// 取前7個元素
        .sorted()	// 排序
        .forEach(System.out::println);	// 根據傳遞給它的函式對流中每個物件執行操作
}

}
通過上面的示例,我們可以發現流有如下特點:

流本身不儲存元素,並且不會改變源物件,相反,它會返回一個持有結果的新流
流可以在不使用賦值或可變資料的情況下對有狀態的系統建模
流是一種宣告式程式設計風格,它宣告想要做什麼,而非指明如何做
流的迭代過稱為內部迭代,你看不到迭代過程,可讀性更強
流是懶載入的,它會等到需要時才執行

建立流的方式有很多,下面逐個介紹:

通過 Stream.of() 可以很容易地將一組元素轉化為流

Stream.of(new Bubble(1), new Bubble(2), new Bubble(3)).forEach(System.out::println);
Stream.of(“a”, “b”, “c”, “d”, “e”, “f”).forEach(System.out::print);
Stream.of(3.14159, 2.718, 1.618).forEach(System.out::println);
每個集合也可以通過呼叫 stream() 方法來產生一個流

List list = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
list.stream().forEach(System.out::print);
Set set = new HashSet<>(Arrays.asList(“a”, “b”, “c”, “d”, “e”, “f”));
set.stream().forEach(System.out::print);
使用 Stream.generate() 搭配 Supplier 生成 T 型別的流

Stream.generate(Math::random).limit(10).forEach(System.out::print);
Stream.iterate() 產生的流的第一個元素是種子,然後把種子傳遞給方法,方法的執行結果被新增到流,並作為下次呼叫 iterate() 的第一個引數

Stream.iterate(0, n -> n + 1).limit(10).forEach(System.out::print)
使用 Stream.generate() 和 Stream.iterate() 生成的無限流一定要用 limit() 截斷

使用建造者模式建立一個 builder 物件,然後將建立流所需的多個資訊傳遞給它,最後 builder 物件執行建立流的操作

Stream.Builder builder = Stream.builder();
builder.add(“a”);
builder.add(“b”);

builder.build(); // 建立流
// builder.add(“c”) // 呼叫 build() 方法後繼續新增元素會產生異常
Arrays 類中有一個名為 stream() 的靜態方法用於把陣列轉換成流

Arrays.stream(new double[] {3.14159, 2.718, 1.618}).forEach(System.out::print);
Arrays.stream(new int[] {1, 3, 5}).forEach(System.out::print);
Arrays.stream(new long[] {11, 22, 44, 66}).forEach(System.out::print);
// 選擇一個子域
Arrays.stream(new int[] {1, 3, 5, 7, 15, 28, 37}, 3, 6).forEach(System.out::print);
最後一次 stream() 的呼叫有兩個額外的引數,第一個引數告訴 stream() 從陣列的哪個位置開始選擇元素,第二個引數告知在哪裡停止

IntStream 類提供 range() 方法用於生成整型序列的流,編寫迴圈時,這個方法會更加便利

IntStream.range(10, 20).sum(); // 求得 10 - 20 的序列和
IntStream.range(10, 20).forEach(System.out::print); // 迴圈輸出 10 - 20
Random 類被一組生成流的方式增強了,可以生成一組隨機數流

Random rand = new Random(47);
// 產生一個隨機流
rand.ints().boxed();
// 控制上限和下限
rand.ints(10, 20).boxed();
// 控制流的大小
rand.ints(2).boxed();
// 控制流的大小和界限
rand.ints(3, 3, 9).boxed();
Random 類除了能生成基本型別 int,long,double 的流,使用 boxed() 操作會自動把基本型別包裝為對應的裝箱型別

Java8 在 java.util.regex.Pattern 中新增了一個方法 splitAsStream(),這個方法可以根據傳入的公式將字元序列轉化為流

Pattern.compile("[.,?]+").splitAsStream(“a,b,c,d,e”).forEach(System.out::print);

中間操作具體包括去重、過濾、對映等操作,作用於從流中獲取的每一個物件,並返回一個新的流物件。

peek() 操作的目的是幫助除錯,它允許你無修改地檢視流中的元素

Stream.of(“a b c d e”.split(" ")).map(w -> w + " ").peek(System.out::print);
sorted() 可以幫助我們實現對流元素的排序,如果不使用預設的自然排序,則需要傳入一個比較器,也可以把 Lambda 函式作為引數傳遞給 sorted()

Stream.of(“a b c d e”.split(" ")).sorted(Comparator.reverseOrder())
.map(w -> w + " ").peek(System.out::print);
distinct() 可用於消除流中的重複元素

new Random(47).ints(5, 20).distinct().limit(7).forEach(System.out::println);
filter(Predicate) 將元素傳遞給過濾函式,若結果為 true,則保留元素

// 檢測質數
Stream.iterate(2, n -> n + 1).filter(i -> i % 2 ==0)
.limit(10).forEach(System.out::print)
map(Function) 將函式操作應用到輸入流的元素,並將返回值傳遞到輸出流

Arrays.stream(new String[] {“12”, “23”, “34”}).map(s -> “[” + s + “]”)
.forEach(System.out::print)
另外還有 mapToInt(ToIntFunction)、mapToLong(ToLongFunction)、mapToDouble(ToDoubleFunction),操作和 map(Function) 相似,只是結果流為各自對應的基本型別

如果在將函式應用到元素的過程中丟擲了異常,此時會把原始元素放到輸出流

使用 flatMap() 將產生流的函式應用在每個元素上,然後將產生每個流都扁平化為元素

Stream.of(1, 2, 3).flatMap(i -> Stream.of(“hello” + i)).forEach(System.out::println);
另外還有 flatMapToInt(Function)、flatMapToLong(Function)、flatMapToDouble(Function),操作和 flatMap() 相似,只是結果元素為各自對應的基本型別

如果在一個空流中嘗試獲取元素,結果肯定是得到一個異常。我們希望可以得到友好的提示,而不是糊你一臉 NullPointException。Optional 的出現就是為了解決臭名昭著的空指標異常

一些標準流操作返回 Optional 物件,因為它們不能保證預期結果一定存在,包括:

findFirst()

返回一個包含第一個元素的 Optional 物件,如果流為空則返回 Optional.empty

findAny()

返回包含任意元素的 Optional 物件,如果流為空則返回 Optional.empty

max() 和 min()

返回一個包含最大值或者最小值的 Optional 物件,如果流為空則返回 Optional.empty

reduce(Function)

將函式的返回值包裝在 Optional 中

Optional 類本質上是一個容器物件,所謂容器是指:它可以儲存型別 T 的值,也可以儲存一個 null。此外,Optional 提供了許多有用的方法,可以幫助我們不用顯示地進行空值檢測:

ifPresent()

是否有值存在,存在放回 true,否則返回 false

ifPresent(Consumer)

當值存在時呼叫 Consumer,否則什麼也不做

orElse(otherObject)

如果值存在則直接返回,否則生成 otherObject

orElseGet(Supplier)

如果值存在則直接返回,否則使用 Supplier 函式生成一個可替代物件

orElseThrow(Supplier)

如果值存在則直接返回,否則使用 Supplier 函式生成一個異常

下面是對 Optional 的一個簡單應用

class OptionalBasics {

static void test(Optional<String> optString) {
    if(optString.isPresent())
        System.out.println(optString.get()); 
    else
        System.out.println("Nothing inside!");
}

public static void main(String[] args) {
    test(Stream.of("Epithets").findFirst());
    test(Stream.<String>empty().findFirst());	// 生成一個空流
}

}
當我們需要在自己的程式碼中加入 Optional 時,可以使用下面三個靜態方法:

empty()

生成一個空 Optional

of(value)

將一個非空值包裝到 Optional 裡

ofNullable(value)

針對一個可能為空的值,為空時自動生成 Optional.empty,否則將值包裝在 Optional 中

當我們的流管道生成 Optional 物件,下面三個方法可以使得 Optional 能做更多後續操作:

filter(Predicate)

對 Optional 中的內容應用 Predicate 並將結果返回。如果 Optional 不滿足 Predicate,將 Optional 轉化為空 Optional 。如果 Optional 已經為空,則直接返回空 Optional

map(Function)

如果 Optional 不為空,應用 Function 於 Optional 中的內容,並返回結果,否則直接返回 Optional.empty

flatMap(Function)

一般應用於已生成 Optional 的對映函式,所以 flatMap() 不會像 map() 那樣將結果封裝在 Optional 中

終端操作將獲取流的最終結果,至此我們無法再繼續往後傳遞流。可以說,終端操作總是我們在使用流時所做的最後一件事

當我們需要得到陣列型別的資料以便於後續操作時,可以使用下述方法產生陣列:

toArray()

將流轉換成適當型別的陣列

toArray(generetor)

生成自定義型別的陣列

常見的如 forEach(Consumer),另外還有 forEachOrdered(Consumer),保證按照原始流的順序操作。第二種形式僅在引入並行流時才有意義。所謂並行流是將流分割為多個,並在不同的處理器上分別執行。由於多處理器並行操作的原因,輸出的結果可能會不一樣,因此需要用到 forEachOrdered(Consumer)

在這裡我們只是簡單介紹一下常見的 Collectors 示例,實際上它還有一些非常複雜的實現。大多數情況下,java.util.stream.Collectors 中預設的 Collector 就能滿足我們的需求

collect(Collector)

使用 Collector 收集流元素到結果集合中

collect(Supplier, BiConsumer, BiConsumer)

第一個引數建立一個新的結果集合,第二個引數將下一個元素收集到結果集合中,第三個引數用於將兩個結果集合合併起來

組合意味著將流中所有元素以某種方式組合為一個元素

reduce(BinaryOperator)

使用 BinaryOperator 來組合所有流中的元素。因為流可能為空,其返回值為 Optional

reduce(identity, BinaryOperator)

功能同上,但是使用 identity 作為其組合的初始值。因此如果流為空,identity 就是結果

看一段程式碼示例:

Stream.generate(Math::random).limit(10)
.reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1).ifPresent(System.out::println);
返回的結果是 Optional 型別,Lambda 表示式中的第一個引數 fr0 是 reduce 中上一次呼叫的結果,而第二個引數 fr1 是從流傳遞過來的值

allMatch(Predicate)

如果流的每個元素提供給 Predicate 都返回 true ,結果返回為 true。在第一個 false 時,則停止執行計算

anyMatch(Predicate)

如果流的任意一個元素提供給 Predicate 返回 true ,結果返回為 true。在第一個 true 是停止執行計算

noneMatch(Predicate)

如果流的每個元素提供給 Predicate 都返回 false 時,結果返回為 true。在第一個 true 時停止執行計算

findFirst()

返回第一個流元素的 Optional,如果流為空返回 Optional.empty

findAny(

返回含有任意流元素的 Optional,如果流為空返回 Optional.empty

count()

流中的元素個數

max(Comparator)

根據所傳入的 Comparator 所決定的最大元素

min(Comparator)

根據所傳入的 Comparator 所決定的最小元素

average()

求取流元素平均值

max() 和 min()

數值流操作無需 Comparator

sum()

對所有流元素進行求和

相關文章