Java8編程思想-lambda 表達式

第十四章 流式編程

集合優化了對象的存儲,而流和對象的處理有關。

流是一系列與特定存儲機制無關的元素——實際上,流並沒有“存儲”之說。

利用流,我們無需迭代集合中的元素,就可以提取和操作它們。這些管道通常被組合在一起,在流上形成一條操作管道。

在大多數情況下,將對象存儲在集合中是爲了處理他們,因此你將會發現你將把編程的主要焦點從集合轉移到了流上。流的一個核心好處是,它使得程序更加短小並且更易理解。當 Lambda 表達式和方法引用(method references)和流一起使用的時候會讓人感覺自成一體。流使得 Java 8 更具吸引力。

舉個例子,假如你要隨機展示 5 至 20 之間不重複的整數並進行排序。實際上,你的關注點首先是創建一個有序集合。圍繞這個集合進行後續的操作。但是使用流式編程,你就可以簡單陳述你想做什麼:

// streams/Randoms.java
import java.util.*;
public class Randoms {
    public static void main(String[] args) {
        new Random(47)
            .ints(5, 20)
            .distinct()
            .limit(7)
            .sorted()
            .forEach(System.out::println);
    }
}

輸出結果:

6
10
13
16
17
18
19

首先,我們給 Random 對象一個種子(以便程序再次運行時產生相同的輸出)。ints() 方法產生一個流並且 ints() 方法有多種方式的重載 — 兩個參數限定了數值產生的邊界。這將生成一個整數流。我們可以使用中間流操作(intermediate stream operation) distinct() 來獲取它們的非重複值,然後使用 limit() 方法獲取前 7 個元素。接下來,我們使用 sorted() 方法排序。最終使用 forEach() 方法遍歷輸出,它根據傳遞給它的函數對每個流對象執行操作。在這裏,我們傳遞了一個可以在控制檯顯示每個元素的方法引用。System.out::println

注意 Randoms.java 中沒有聲明任何變量。流可以在不使用賦值或可變數據的情況下對有狀態的系統建模,這非常有用。

聲明式編程(Declarative programming)是一種:聲明要做什麼,而非怎麼做的編程風格。正如我們在函數式編程中所看到的。注意,命令式編程的形式更難以理解。代碼示例:

// streams/ImperativeRandoms.java
import java.util.*;
public class ImperativeRandoms {
    public static void main(String[] args) {
        Random rand = new Random(47);
        SortedSet<Integer> rints = new TreeSet<>();
        while(rints.size() < 7) {
            int r = rand.nextInt(20);
            if(r < 5) continue;
            rints.add(r);
        }
        System.out.println(rints);
    }
}

輸出結果:

[7, 8, 9, 11, 13, 15, 18]

Randoms.java 中,我們無需定義任何變量,但在這裏我們定義了 3 個變量: randrintsr。由於 nextInt() 方法沒有下限的原因(其內置的下限永遠爲 0),這段代碼實現起來更復雜。所以我們要生成額外的值來過濾小於 5 的結果。

注意,你必須要研究程序的真正意圖,而在 Randoms.java 中,代碼只是告訴了你它正在做什麼。這種語義清晰性也是 Java 8 的流式編程更受推崇的重要原因。

ImperativeRandoms.java 中顯式地編寫迭代機制稱爲外部迭代。而在 Randoms.java 中,流式編程採用內部迭代,這是流式編程的核心特性之一。這種機制使得編寫的代碼可讀性更強,也更能利用多核處理器的優勢。通過放棄對迭代過程的控制,我們把控制權交給並行化機制。我們將在併發編程一章中學習這部分內容。

另一個重要方面,流是懶加載的。這代表着它只在絕對必要時才計算。你可以將流看作“延遲列表”。由於計算延遲,流使我們能夠表示非常大(甚至無限)的序列,而不需要考慮內存問題。

流支持

Java 設計者面臨着這樣一個難題:現存的大量類庫不僅爲 Java 所用,同時也被應用在整個 Java 生態圈數百萬行的代碼中。如何將一個全新的流的概念融入到現有類庫中呢?

比如在 Random 中添加更多的方法。只要不改變原有的方法,現有代碼就不會受到干擾。

問題是,接口部分怎麼改造呢?特別是涉及集合類接口的部分。如果你想把一個集合轉換爲流,直接向接口添加新方法會破壞所有老的接口實現類。

Java 8 採用的解決方案是:在接口中添加被 default默認)修飾的方法。通過這種方案,設計者們可以將流式(stream)方法平滑地嵌入到現有類中。流方法預置的操作幾乎已滿足了我們平常所有的需求。流操作的類型有三種:創建流,修改流元素(中間操作, Intermediate Operations),消費流元素(終端操作, Terminal Operations)。最後一種類型通常意味着收集流元素(通常是到集合中)。

下面我們來看下每種類型的流操作。

流創建

你可以通過 Stream.of() 很容易地將一組元素轉化成爲流(Bubble 類在本章的後面定義):

// streams/StreamOf.java
import java.util.stream.*;
public class StreamOf {
    public static void main(String[] args) {
        Stream.of(new Bubble(1), new Bubble(2), new Bubble(3))
            .forEach(System.out::println);
        Stream.of("It's ", "a ", "wonderful ", "day ", "for ", "pie!")
            .forEach(System.out::print);
        System.out.println();
        Stream.of(3.14159, 2.718, 1.618)
            .forEach(System.out::println);
    }
}

輸出結果:

Bubble(1)
Bubble(2)
Bubble(3)
It's a wonderful day for pie!
3.14159
2.718
1.618

除此之外,每個集合都可以通過調用 stream() 方法來產生一個流。代碼示例:

// streams/CollectionToStream.java
import java.util.*;
import java.util.stream.*;
public class CollectionToStream {
    public static void main(String[] args) {
        List<Bubble> bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
        System.out.println(bubbles.stream()
            .mapToInt(b -> b.i)
            .sum());
        
        Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
        w.stream()
         .map(x -> x + " ")
         .forEach(System.out::print);
        System.out.println();
        
        Map<String, Double> m = new HashMap<>();
        m.put("pi", 3.14159);
        m.put("e", 2.718);
        m.put("phi", 1.618);
        m.entrySet().stream()
                    .map(e -> e.getKey() + ": " + e.getValue())
                    .forEach(System.out::println);
    }
}

輸出結果:

6
a pie! It's for wonderful day
phi: 1.618
e: 2.718
pi: 3.14159

在創建 List<Bubble> 對象之後,我們只需要簡單地調用所有集合中都有的 stream()。中間操作 map() 會獲取流中的所有元素,並且對流中元素應用操作從而產生新的元素,並將其傳遞到後續的流中。通常 map() 會獲取對象併產生新的對象,但在這裏產生了特殊的用於數值類型的流。例如,mapToInt() 方法將一個對象流(object stream)轉換成爲包含整型數字的 IntStream。同樣,針對 FloatDouble 也有類似名字的操作。

我們通過調用字符串的 split()(該方法會根據參數來拆分字符串)來獲取元素用於定義變量 w。稍後你會知道 split() 參數可以是十分複雜,但在這裏我們只是根據空格來分割字符串。

爲了從 Map 集合中產生流數據,我們首先調用 entrySet() 產生一個對象流,每個對象都包含一個 key 鍵以及與其相關聯的 value 值。然後分別調用 getKey()getValue() 獲取值。

隨機數流

Random 類被一組生成流的方法增強了。代碼示例:

// streams/RandomGenerators.java
import java.util.*;
import java.util.stream.*;
public class RandomGenerators {
    public static <T> void show(Stream<T> stream) {
        stream
        .limit(4)
        .forEach(System.out::println);
        System.out.println("++++++++");
    }
    
    public static void main(String[] args) {
        Random rand = new Random(47);
        show(rand.ints().boxed());
        show(rand.longs().boxed());
        show(rand.doubles().boxed());
        // 控制上限和下限:
        show(rand.ints(10, 20).boxed());
        show(rand.longs(50, 100).boxed());
        show(rand.doubles(20, 30).boxed());
        // 控制流大小:
        show(rand.ints(2).boxed());
        show(rand.longs(2).boxed());
        show(rand.doubles(2).boxed());
        // 控制流的大小和界限
        show(rand.ints(3, 3, 9).boxed());
        show(rand.longs(3, 12, 22).boxed());
        show(rand.doubles(3, 11.5, 12.3).boxed());
    }
}

輸出結果:

-1172028779
1717241110
-2014573909
229403722
++++++++
2955289354441303771
3476817843704654257
-8917117694134521474
4941259272818818752
++++++++
0.2613610344283964
0.0508673570556899
0.8037155449603999
0.7620665811558285
++++++++
16
10
11
12
++++++++
65
99
54
58
++++++++
29.86777681078574
24.83968447804611
20.09247112332014
24.046793846338723
++++++++
1169976606
1947946283
++++++++
2970202997824602425
-2325326920272830366
++++++++
0.7024254510631527
0.6648552384607359
++++++++
6
7
7
++++++++
17
12
20
++++++++
12.27872414236691
11.732085449736195
12.196509449817267
++++++++

爲了消除冗餘代碼,我創建了一個泛型方法 show(Stream<T> stream) (在講解泛型之前就使用這個特性,確實有點作弊,但是回報是值得的)。類型參數 T 可以是任何類型,所以這個方法對 IntegerLongDouble 類型都生效。但是 Random 類只能生成基本類型 intlongdouble 的流。幸運的是, boxed() 流操作將會自動地把基本類型包裝成爲對應的裝箱類型,從而使得 show() 能夠接受流。

我們可以使用 Random 爲任意對象集合創建 Supplier。如下是一個文本文件提供字符串對象的例子。

Cheese.dat 文件內容:

// streams/Cheese.dat
Not much of a cheese shop really, is it?
Finest in the district, sir.
And what leads you to that conclusion?
Well, it's so clean.
It's certainly uncontaminated by cheese.

我們通過 File 類將 Cheese.dat 文件的所有行讀取到 List<String> 中。代碼示例:

// streams/RandomWords.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
import java.io.*;
import java.nio.file.*;
public class RandomWords implements Supplier<String> {
    List<String> words = new ArrayList<>();
    Random rand = new Random(47);
    RandomWords(String fname) throws IOException {
        List<String> lines = Files.readAllLines(Paths.get(fname));
        // 略過第一行
        for (String line : lines.subList(1, lines.size())) {
            for (String word : line.split("[ .?,]+"))
                words.add(word.toLowerCase());
        }
    }
    public String get() {
        return words.get(rand.nextInt(words.size()));
    }
    @Override
    public String toString() {
        return words.stream()
            .collect(Collectors.joining(" "));
    }
    public static void main(String[] args) throws Exception {
        System.out.println(
            Stream.generate(new RandomWords("Cheese.dat"))
                .limit(10)
                .collect(Collectors.joining(" ")));
    }
}

輸出結果:

it shop sir the much cheese by conclusion district is

在這裏你可以看到更爲複雜的 split() 運用。在構造器中,每一行都被 split() 通過空格或者被方括號包裹的任意標點符號進行分割。在結束方括號後面的 + 代表 + 前面的東西可以出現一次或者多次。

我們注意到在構造函數中循環體使用命令式編程(外部迭代)。在以後的例子中,你甚至會看到我們如何消除這一點。這種舊的形式雖不是特別糟糕,但使用流會讓人感覺更好。

toString() 和主方法中你看到了 collect() 收集操作,它根據參數來組合所有流中的元素。

當你使用 Collectors.joining(),你將會得到一個 String 類型的結果,每個元素都根據 joining() 的參數來進行分割。還有許多不同的 Collectors 用於產生不同的結果。

在主方法中,我們提前看到了 Stream.generate() 的用法,它可以把任意 Supplier<T> 用於生成 T 類型的流。

int 類型的範圍

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

// streams/Ranges.java
import static java.util.stream.IntStream.*;
public class Ranges {
    public static void main(String[] args) {
        // 傳統方法:
        int result = 0;
        for (int i = 10; i < 20; i++)
            result += i;
        System.out.println(result);
        // for-in 循環:
        result = 0;
        for (int i : range(10, 20).toArray())
            result += i;
        System.out.println(result);
        // 使用流:
        System.out.println(range(10, 20).sum());
    }
}

輸出結果:

145
145
145

在主方法中的第一種方式是我們傳統編寫 for 循環的方式;第二種方式,我們使用 range() 創建了流並將其轉化爲數組,然後在 for-in 代碼塊中使用。但是,如果你能像第三種方法那樣全程使用流是更好的。我們對範圍中的數字進行求和。在流中可以很方便的使用 sum() 操作求和。

注意 IntStream.range() 相比 onjava.Range.range() 擁有更多的限制。這是由於其可選的第三個參數,後者允許步長大於 1,並且可以從大到小來生成。

實用小功能 repeat() 可以用來替換簡單的 for 循環。代碼示例:

// onjava/Repeat.java
package onjava;
import static java.util.stream.IntStream.*;
public class Repeat {
    public static void repeat(int n, Runnable action) {
        range(0, n).forEach(i -> action.run());
    }
}

其產生的循環更加清晰:

// streams/Looping.java
import static onjava.Repeat.*;
public class Looping {
    static void hi() {
        System.out.println("Hi!");
    }
    public static void main(String[] args) {
        repeat(3, () -> System.out.println("Looping!"));
        repeat(2, Looping::hi);
    }
}

輸出結果:

Looping!
Looping!
Looping!
Hi!
Hi!

原則上,在代碼中包含並解釋 repeat() 並不值得。誠然它是一個相當透明的工具,但結果取決於你的團隊和公司的運作方式。

generate()

參照 RandomWords.javaStream.generate() 搭配 Supplier<T> 使用的例子。代碼示例:

// streams/Generator.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Generator implements Supplier<String> {
    Random rand = new Random(47);
    char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
    
    public String get() {
        return "" + letters[rand.nextInt(letters.length)];
    }
    
    public static void main(String[] args) {
        String word = Stream.generate(new Generator())
                            .limit(30)
                            .collect(Collectors.joining());
        System.out.println(word);
    }
}

輸出結果:

YNZBRNYGCFOWZNTCQRGSEGZMMJMROE

使用 Random.nextInt() 方法來挑選字母表中的大寫字母。Random.nextInt() 的參數代表可以接受的最大的隨機數範圍,所以使用數組邊界是經過深思熟慮的。

如果要創建包含相同對象的流,只需要傳遞一個生成那些對象的 lambdagenerate() 中:

// streams/Duplicator.java
import java.util.stream.*;
public class Duplicator {
    public static void main(String[] args) {
        Stream.generate(() -> "duplicate")
              .limit(3)
              .forEach(System.out::println);
    }
}

輸出結果:

duplicate
duplicate
duplicate

如下是在本章之前例子中使用過的 Bubble 類。注意它包含了自己的靜態生成器(Static generator)方法。

// streams/Bubble.java
import java.util.function.*;
public class Bubble {
    public final int i;
    
    public Bubble(int n) {
        i = n;
    }
    
    @Override
    public String toString() {
        return "Bubble(" + i + ")";
    }
    
    private static int count = 0;
    public static Bubble bubbler() {
        return new Bubble(count++);
    }
}

由於 bubbler()Supplier<Bubble> 是接口兼容的,我們可以將其方法引用直接傳遞給 Stream.generate()

// streams/Bubbles.java
import java.util.stream.*;
public class Bubbles {
    public static void main(String[] args) {
        Stream.generate(Bubble::bubbler)
              .limit(5)
              .forEach(System.out::println);
    }
}

輸出結果:

Bubble(0)
Bubble(1)
Bubble(2)
Bubble(3)
Bubble(4)

這是創建單獨工廠類(Separate Factory class)的另一種方式。在很多方面它更加整潔,但是這是一個對於代碼組織和品味的問題——你總是可以創建一個完全不同的工廠類。

iterate()

Stream.iterate() 以種子(第一個參數)開頭,並將其傳給方法(第二個參數)。方法的結果將添加到流,並存儲作爲第一個參數用於下次調用 iterate(),依次類推。我們可以利用 iterate() 生成一個斐波那契數列。代碼示例:

// streams/Fibonacci.java
import java.util.stream.*;
public class Fibonacci {
    int x = 1;
    
    Stream<Integer> numbers() {
        return Stream.iterate(0, i -> {
            int result = x + i;
            x = i;
            return result;
        });
    }
    
    public static void main(String[] args) {
        new Fibonacci().numbers()
                       .skip(20) // 過濾前 20 個
                       .limit(10) // 然後取 10 個
                       .forEach(System.out::println);
    }
}

輸出結果:

6765
10946
17711
28657
46368
75025
121393
196418
317811
514229

斐波那契數列將數列中最後兩個元素進行求和以產生下一個元素。iterate() 只能記憶結果,因此我們需要利用一個變量 x 追蹤另外一個元素。

在主方法中,我們使用了一個之前沒有見過的 skip() 操作。它根據參數丟棄指定數量的流元素。在這裏,我們丟棄了前 20 個元素。

流的建造者模式

在建造者設計模式(也稱構造器模式)中,首先創建一個 builder 對象,傳遞給它多個構造器信息,最後執行“構造”。Stream 庫提供了這樣的 Builder。在這裏,我們重新審視文件讀取並將其轉換成爲單詞流的過程。代碼示例:

// streams/FileToWordsBuilder.java
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;

public class FileToWordsBuilder {
    Stream.Builder<String> builder = Stream.builder();
    
    public FileToWordsBuilder(String filePath) throws Exception {
        Files.lines(Paths.get(filePath))
             .skip(1) // 略過開頭的註釋行
             .forEach(line -> {
                  for (String w : line.split("[ .?,]+"))
                      builder.add(w);
              });
    }
    
    Stream<String> stream() {
        return builder.build();
    }
    
    public static void main(String[] args) throws Exception {
        new FileToWordsBuilder("Cheese.dat")
            .stream()
            .limit(7)
            .map(w -> w + " ")
            .forEach(System.out::print);
    }
}

輸出結果:

Not much of a cheese shop really

注意,構造器會添加文件中的所有單詞(除了第一行,它是包含文件路徑信息的註釋),但是其並沒有調用 build()。只要你不調用 stream() 方法,就可以繼續向 builder 對象中添加單詞。

在該類的更完整形式中,你可以添加一個標誌位用於查看 build() 是否被調用,並且可能的話增加一個可以添加更多單詞的方法。在 Stream.Builder 調用 build() 方法後繼續嘗試添加單詞會產生一個異常。

Arrays

Arrays 類中含有一個名爲 stream() 的靜態方法用於把數組轉換成爲流。我們可以重寫 interfaces/Machine.java 中的主方法用於創建一個流,並將 execute() 應用於每一個元素。代碼示例:

// streams/Machine2.java
import java.util.*;
import onjava.Operations;
public class Machine2 {
    public static void main(String[] args) {
        Arrays.stream(new Operations[] {
            () -> Operations.show("Bing"),
            () -> Operations.show("Crack"),
            () -> Operations.show("Twist"),
            () -> Operations.show("Pop")
        }).forEach(Operations::execute);
    }
}

輸出結果:

Bing
Crack
Twist
Pop

new Operations[] 表達式動態創建了 Operations 對象的數組。

stream() 同樣可以產生 IntStreamLongStreamDoubleStream

// streams/ArrayStreams.java
import java.util.*;
import java.util.stream.*;

public class ArrayStreams {
    public static void main(String[] args) {
        Arrays.stream(new double[] { 3.14159, 2.718, 1.618 })
            .forEach(n -> System.out.format("%f ", n));
        System.out.println();
        
        Arrays.stream(new int[] { 1, 3, 5 })
            .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        
        Arrays.stream(new long[] { 11, 22, 44, 66 })
            .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        
        // 選擇一個子域:
        Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6)
            .forEach(n -> System.out.format("%d ", n));
    }
}

輸出結果:

3.141590 2.718000 1.618000
1 3 5
11 22 44 66
7 15 28

最後一次 stream() 的調用有兩個額外的參數。第一個參數告訴 stream() 從數組的哪個位置開始選擇元素,第二個參數用於告知在哪裏停止。每種不同類型的 stream() 都有類似的操作。

正則表達式

Java 的正則表達式將在字符串這一章節詳細介紹。Java 8 在 java.util.regex.Pattern 中增加了一個新的方法 splitAsStream()。這個方法可以根據傳入的公式將字符序列轉化爲流。但是有一個限制,輸入只能是 CharSequence,因此不能將流作爲 splitAsStream() 的參數。

我們再一次查看將文件處理爲單詞流的過程。這一次,我們使用流將文件分割爲單獨的字符串,接着使用正則表達式將字符串轉化爲單詞流。

// streams/FileToWordsRegexp.java
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;
import java.util.regex.Pattern;
public class FileToWordsRegexp {
    private String all;
    public FileToWordsRegexp(String filePath) throws Exception {
        all = Files.lines(Paths.get(filePath))
        .skip(1) // First (comment) line
        .collect(Collectors.joining(" "));
    }
    public Stream<String> stream() {
        return Pattern
        .compile("[ .,?]+").splitAsStream(all);
    }
    public static void
    main(String[] args) throws Exception {
        FileToWordsRegexp fw = new FileToWordsRegexp("Cheese.dat");
        fw.stream()
          .limit(7)
          .map(w -> w + " ")
          .forEach(System.out::print);
        fw.stream()
          .skip(7)
          .limit(2)
          .map(w -> w + " ")
          .forEach(System.out::print);
    }
}

輸出結果:

Not much of a cheese shop really is it

在構造器中我們讀取了文件中的所有內容(跳過第一行註釋,並將其轉化成爲單行字符串)。現在,當你調用 stream() 的時候,可以像往常一樣獲取一個流,但這次你可以多次調用 stream() 在已存儲的字符串中創建一個新的流。這裏有個限制,整個文件必須存儲在內存中;在大多數情況下這並不是什麼問題,但是這損失了流操作非常重要的優勢:

  1. 流“不需要存儲”。當然它們需要一些內部存儲,但是這只是序列的一小部分,和持有整個序列並不相同。
  2. 它們是懶加載計算的。

幸運的是,我們稍後就會知道如何解決這個問題。

中間操作

中間操作用於從一個流中獲取對象,並將對象作爲另一個流從後端輸出,以連接到其他操作。

跟蹤和調試

peek() 操作的目的是幫助調試。它允許你無修改地查看流中的元素。代碼示例:

// streams/Peeking.java
class Peeking {
    public static void main(String[] args) throws Exception {
        FileToWords.stream("Cheese.dat")
        .skip(21)
        .limit(4)
        .map(w -> w + " ")
        .peek(System.out::print)
        .map(String::toUpperCase)
        .peek(System.out::print)
        .map(String::toLowerCase)
        .forEach(System.out::print);
    }
}

輸出結果:

Well WELL well it IT it s S s so SO so

FileToWords 稍後定義,但它的功能實現貌似和之前我們看到的差不多:產生字符串對象的流。之後在其通過管道時調用 peek() 進行處理。

因爲 peek() 符合無返回值的 Consumer 函數式接口,所以我們只能觀察,無法使用不同的元素來替換流中的對象。

流元素排序

Randoms.java 中,我們熟識了 sorted() 的默認比較器實現。其實它還有另一種形式的實現:傳入一個 Comparator 參數。代碼示例:

// streams/SortedComparator.java
import java.util.*;
public class SortedComparator {
    public static void main(String[] args) throws Exception {
        FileToWords.stream("Cheese.dat")
        .skip(10)
        .limit(10)
        .sorted(Comparator.reverseOrder())
        .map(w -> w + " ")
        .forEach(System.out::print);
    }
}

輸出結果:

you what to the that sir leads in district And

sorted() 預設了一些默認的比較器。這裏我們使用的是反轉“自然排序”。當然你也可以把 Lambda 函數作爲參數傳遞給 sorted()

移除元素

  • distinct():在 Randoms.java 類中的 distinct() 可用於消除流中的重複元素。相比創建一個 Set 集合,該方法的工作量要少得多。

  • filter(Predicate):過濾操作會保留與傳遞進去的過濾器函數計算結果爲 true 元素。

在下例中,isPrime() 作爲過濾器函數,用於檢測質數。

// streams/Prime.java
import java.util.stream.*;
import static java.util.stream.LongStream.*;
public class Prime {
    public static Boolean isPrime(long n) {
        return rangeClosed(2, (long)Math.sqrt(n))
        .noneMatch(i -> n % i == 0);
    }
    public LongStream numbers() {
        return iterate(2, i -> i + 1)
        .filter(Prime::isPrime);
    }
    public static void main(String[] args) {
        new Prime().numbers()
        .limit(10)
        .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        new Prime().numbers()
        .skip(90)
        .limit(10)
        .forEach(n -> System.out.format("%d ", n));
    }
}

輸出結果:

2 3 5 7 11 13 17 19 23 29
467 479 487 491 499 503 509 521 523 541

rangeClosed() 包含了上限值。如果不能整除,即餘數不等於 0,則 noneMatch() 操作返回 true,如果出現任何等於 0 的結果則返回 falsenoneMatch() 操作一旦有失敗就會退出。

應用函數到元素

  • map(Function):將函數操作應用在輸入流的元素中,並將返回值傳遞到輸出流中。

  • mapToInt(ToIntFunction):操作同上,但結果是 IntStream

  • mapToLong(ToLongFunction):操作同上,但結果是 LongStream

  • mapToDouble(ToDoubleFunction):操作同上,但結果是 DoubleStream

在這裏,我們使用 map() 映射多種函數到一個字符串流中。代碼示例:

// streams/FunctionMap.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class FunctionMap {
    static String[] elements = { "12", "", "23", "45" };
    static Stream<String>
    testStream() {
        return Arrays.stream(elements);
    }
    static void test(String descr, Function<String, String> func) {
        System.out.println(" ---( " + descr + " )---");
        testStream()
        .map(func)
        .forEach(System.out::println);
    }
    public static void main(String[] args) {
        test("add brackets", s -> "[" + s + "]");
        test("Increment", s -> {
            try {
                return Integer.parseInt(s) + 1 + "";
            }
            catch(NumberFormatException e) {
                return s;
            }
        }
        );
        test("Replace", s -> s.replace("2", "9"));
        test("Take last digit", s -> s.length() > 0 ?
        s.charAt(s.length() - 1) + "" : s);
    }
}

輸出結果:

---( add brackets )---
[12]
[]
[23]
[45]
---( Increment )---
13
24
46
---( Replace )---
19
93
45
---( Take last digit )---
2
3
5

在上面的自增示例中,我們使用 Integer.parseInt() 嘗試將一個字符串轉化爲整數。如果字符串不能轉化成爲整數就會拋出 NumberFormatException 異常,我們只須回過頭來將原始字符串放回到輸出流中。

在以上例子中,map() 將一個字符串映射爲另一個字符串,但是我們完全可以產生和接收類型完全不同的類型,從而改變流的數據類型。下面代碼示例:

// streams/FunctionMap2.java
// Different input and output types (不同的輸入輸出類型)
import java.util.*;
import java.util.stream.*;
class Numbered {
    final int n;
    Numbered(int n) {
        this.n = n;
    }
    @Override
    public String toString() {
        return "Numbered(" + n + ")";
    }
}
class FunctionMap2 {
    public static void main(String[] args) {
        Stream.of(1, 5, 7, 9, 11, 13)
        .map(Numbered::new)
        .forEach(System.out::println);
    }
}

輸出結果:

Numbered(1)
Numbered(5)
Numbered(7)
Numbered(9)
Numbered(11)
Numbered(13)

我們將獲取到的整數通過構造器 Numbered::new 轉化成爲 Numbered 類型。

如果使用 Function 返回的結果是數值類型的一種,我們必須使用合適的 mapTo數值類型 進行替代。代碼示例:

// streams/FunctionMap3.java
// Producing numeric output streams( 產生數值輸出流)
import java.util.*;
import java.util.stream.*;
class FunctionMap3 {
    public static void main(String[] args) {
        Stream.of("5", "7", "9")
        .mapToInt(Integer::parseInt)
        .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        Stream.of("17", "19", "23")
        .mapToLong(Long::parseLong)
        .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        Stream.of("17", "1.9", ".23")
        .mapToDouble(Double::parseDouble)
        .forEach(n -> System.out.format("%f ", n));
    }
}

輸出結果:

5 7 9
17 19 23
17.000000 1.900000 0.230000

遺憾的是,Java 設計者並沒有盡最大努力去消除基本類型。

map() 中組合流

假設我們現在有了一個傳入的元素流,並且打算對流元素使用 map() 函數。現在你已經找到了一些可愛並獨一無二的函數功能,但是問題來了:這個函數功能是產生一個流。我們想要產生一個元素流,而實際卻產生了一個元素流的流。

flatMap() 做了兩件事:將產生流的函數應用在每個元素上(與 map() 所做的相同),然後將每個流都扁平化爲元素,因而最終產生的僅僅是元素。

flatMap(Function):當 Function 產生流時使用。

flatMapToInt(Function):當 Function 產生 IntStream 時使用。

flatMapToLong(Function):當 Function 產生 LongStream 時使用。

flatMapToDouble(Function):當 Function 產生 DoubleStream 時使用。

爲了弄清它的工作原理,我們從傳入一個刻意設計的函數給 map() 開始。該函數接受一個整數併產生一個字符串流:

// streams/StreamOfStreams.java
import java.util.stream.*;
public class StreamOfStreams {
    public static void main(String[] args) {
        Stream.of(1, 2, 3)
        .map(i -> Stream.of("Gonzo", "Kermit", "Beaker"))
        .map(e-> e.getClass().getName())
        .forEach(System.out::println);
    }
}

輸出結果:

java.util.stream.ReferencePipeline$Head
java.util.stream.ReferencePipeline$Head
java.util.stream.ReferencePipeline$Head

我們天真地希望能夠得到字符串流,但實際得到的卻是“Head”流的流。我們可以使用 flatMap() 解決這個問題:

// streams/FlatMap.java
import java.util.stream.*;
public class FlatMap {
    public static void main(String[] args) {
        Stream.of(1, 2, 3)
        .flatMap(i -> Stream.of("Gonzo", "Fozzie", "Beaker"))
        .forEach(System.out::println);
    }
}

輸出結果:

Gonzo
Fozzie
Beaker
Gonzo
Fozzie
Beaker
Gonzo
Fozzie
Beaker

從映射返回的每個流都會自動扁平爲組成它的字符串。

下面是另一個演示,我們從一個整數流開始,然後使用每一個整數去創建更多的隨機數。

// streams/StreamOfRandoms.java
import java.util.*;
import java.util.stream.*;
public class StreamOfRandoms {
    static Random rand = new Random(47);
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
            .flatMapToInt(i -> IntStream.concat(
        rand.ints(0, 100).limit(i), IntStream.of(-1)))
            .forEach(n -> System.out.format("%d ", n));
    }
}

輸出結果:

58 -1 55 93 -1 61 61 29 -1 68 0 22 7 -1 88 28 51 89 9 -1

在這裏我們引入了 concat(),它以參數順序組合兩個流。 如此,我們在每個隨機 Integer 流的末尾添加一個 -1 作爲標記。你可以看到最終流確實是從一組扁平流中創建的。

因爲 rand.ints() 產生的是一個 IntStream,所以我必須使用 flatMap()concat()of() 的特定整數形式。

讓我們再看一下將文件劃分爲單詞流的任務。我們最後使用到的是 FileToWordsRegexp.java,它的問題是需要將整個文件讀入行列表中 —— 顯然需要存儲該列表。而我們真正想要的是創建一個不需要中間存儲層的單詞流。

下面,我們再使用 flatMap() 來解決這個問題:

// streams/FileToWords.java
import java.nio.file.*;
import java.util.stream.*;
import java.util.regex.Pattern;
public class FileToWords {
    public static Stream<String> stream(String filePath) throws Exception {
        return Files.lines(Paths.get(filePath))
        .skip(1) // First (comment) line
        .flatMap(line ->
        Pattern.compile("\\W+").splitAsStream(line));
    }
}

stream() 現在是一個靜態方法,因爲它可以自己完成整個流創建過程。

注意\\W+ 是一個正則表達式。他表示“非單詞字符”,+ 表示“可以出現一次或者多次”。小寫形式的 \\w 表示“單詞字符”。

我們之前遇到的問題是 Pattern.compile().splitAsStream() 產生的結果爲流,這意味着當我們只是想要一個簡單的單詞流時,在傳入的行流(stream of lines)上調用 map() 會產生一個單詞流的流。幸運的是,flatMap() 可以將元素流的流扁平化爲一個簡單的元素流。或者,我們可以使用 String.split() 生成一個數組,其可以被 Arrays.stream() 轉化成爲流:

.flatMap(line -> Arrays.stream(line.split("\\W+"))))

有了真正的、而非 FileToWordsRegexp.java 中基於集合存儲的流,我們每次使用都必須從頭創建,因爲流並不能被複用:

// streams/FileToWordsTest.java
import java.util.stream.*;
public class FileToWordsTest {
    public static void main(String[] args) throws Exception {
        FileToWords.stream("Cheese.dat")
        .limit(7)
        .forEach(s -> System.out.format("%s ", s));
        System.out.println();
        FileToWords.stream("Cheese.dat")
        .skip(7)
        .limit(2)
        .forEach(s -> System.out.format("%s ", s));
    }
}

輸出結果:

Not much of a cheese shop really
is it

System.out.format() 中的 %s 表明參數爲 String 類型。

Optional類

在我們學習終端操作之前,我們必須考慮如果你在一個空流中獲取元素會發生什麼。我們喜歡爲了“happy path”而將流連接起來,並假設流不會被中斷。在流中放置 null 是很好的中斷方法。那麼是否有某種對象,可作爲流元素的持有者,即使查看的元素不存在也能友好地提示我們(也就是說,不會發生異常)?

Optional 可以實現這樣的功能。一些標準流操作返回 Optional 對象,因爲它們並不能保證預期結果一定存在。包括:

  • findFirst() 返回一個包含第一個元素的 Optional 對象,如果流爲空則返回 Optional.empty
  • findAny() 返回包含任意元素的 Optional 對象,如果流爲空則返回 Optional.empty
  • max()min() 返回一個包含最大值或者最小值的 Optional 對象,如果流爲空則返回 Optional.empty

reduce() 不再以 identity 形式開頭,而是將其返回值包裝在 Optional 中。(identity 對象成爲其他形式的 reduce() 的默認結果,因此不存在空結果的風險)

對於數字流 IntStreamLongStreamDoubleStreamaverage() 會將結果包裝在 Optional 以防止流爲空。

以下是對空流進行所有這些操作的簡單測試:

// streams/OptionalsFromEmptyStreams.java
import java.util.*;
import java.util.stream.*;
class OptionalsFromEmptyStreams {
    public static void main(String[] args) {
        System.out.println(Stream.<String>empty()
             .findFirst());
        System.out.println(Stream.<String>empty()
             .findAny());
        System.out.println(Stream.<String>empty()
             .max(String.CASE_INSENSITIVE_ORDER));
        System.out.println(Stream.<String>empty()
             .min(String.CASE_INSENSITIVE_ORDER));
        System.out.println(Stream.<String>empty()
             .reduce((s1, s2) -> s1 + s2));
        System.out.println(IntStream.empty()
             .average());
    }
}

輸出結果:

Optional.empty
Optional.empty
Optional.empty
Optional.empty
Optional.empty
OptionalDouble.empty

當流爲空的時候你會獲得一個 Optional.empty 對象,而不是拋出異常。Optional 擁有 toString() 方法可以用於展示有用信息。

注意,空流是通過 Stream.<String>empty() 創建的。如果你在沒有任何上下文環境的情況下調用 Stream.empty(),Java 並不知道它的數據類型;這個語法解決了這個問題。如果編譯器擁有了足夠的上下文信息,比如:

Stream<String> s = Stream.empty();

就可以在調用 empty() 時推斷類型。

這個示例展示了 Optional 的兩個基本用法:

// streams/OptionalBasics.java
import java.util.*;
import java.util.stream.*;
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());
    }
}

輸出結果:

Epithets
Nothing inside!

當你接收到 Optional 對象時,應首先調用 isPresent() 檢查其中是否包含元素。如果存在,可使用 get() 獲取。

便利函數

有許多便利函數可以解包 Optional ,這簡化了上述“對所包含的對象的檢查和執行操作”的過程:

  • ifPresent(Consumer):當值存在時調用 Consumer,否則什麼也不做。
  • orElse(otherObject):如果值存在則直接返回,否則生成 otherObject
  • orElseGet(Supplier):如果值存在則直接返回,否則使用 Supplier 函數生成一個可替代對象。
  • orElseThrow(Supplier):如果值存在直接返回,否則使用 Supplier 函數生成一個異常。

如下是針對不同便利函數的簡單演示:

// streams/Optionals.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class Optionals {
    static void basics(Optional<String> optString) {
        if(optString.isPresent())
            System.out.println(optString.get()); 
        else
            System.out.println("Nothing inside!");
    }
    static void ifPresent(Optional<String> optString) {
        optString.ifPresent(System.out::println);
    }
    static void orElse(Optional<String> optString) {
        System.out.println(optString.orElse("Nada"));
    }
    static void orElseGet(Optional<String> optString) {
        System.out.println(
        optString.orElseGet(() -> "Generated"));
    }
    static void orElseThrow(Optional<String> optString) {
        try {
            System.out.println(optString.orElseThrow(
            () -> new Exception("Supplied")));
        } catch(Exception e) {
            System.out.println("Caught " + e);
        }
    }
    static void test(String testName, Consumer<Optional<String>> cos) {
        System.out.println(" === " + testName + " === ");
        cos.accept(Stream.of("Epithets").findFirst());
        cos.accept(Stream.<String>empty().findFirst());
    }
    public static void main(String[] args) {
        test("basics", Optionals::basics);
        test("ifPresent", Optionals::ifPresent);
        test("orElse", Optionals::orElse);
        test("orElseGet", Optionals::orElseGet);
        test("orElseThrow", Optionals::orElseThrow);
    }
}

輸出結果:

=== basics ===
Epithets
Nothing inside!
=== ifPresent ===
Epithets
=== orElse ===
Epithets
Nada
=== orElseGet ===
Epithets
Generated
=== orElseThrow ===
Epithets
Caught java.lang.Exception: Supplied

test() 通過傳入所有方法都適用的 Consumer 來避免重複代碼。

orElseThrow() 通過 catch 關鍵字來捕獲拋出的異常。更多細節,將在 異常 這一章節中學習。

創建 Optional

當我們在自己的代碼中加入 Optional 時,可以使用下面 3 個靜態方法:

  • empty():生成一個空 Optional
  • of(value):將一個非空值包裝到 Optional 裏。
  • ofNullable(value):針對一個可能爲空的值,爲空時自動生成 Optional.empty,否則將值包裝在 Optional 中。

下面來看看它是如何工作的。代碼示例:

// streams/CreatingOptionals.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class CreatingOptionals {
    static void test(String testName, Optional<String> opt) {
        System.out.println(" === " + testName + " === ");
        System.out.println(opt.orElse("Null"));
    }
    public static void main(String[] args) {
        test("empty", Optional.empty());
        test("of", Optional.of("Howdy"));
        try {
            test("of", Optional.of(null));
        } catch(Exception e) {
            System.out.println(e);
        }
        test("ofNullable", Optional.ofNullable("Hi"));
        test("ofNullable", Optional.ofNullable(null));
    }
}

輸出結果:

=== empty ===
Null
=== of ===
Howdy
java.lang.NullPointerException
=== ofNullable ===
Hi
=== ofNullable ===
Null

我們不能通過傳遞 nullof() 來創建 Optional 對象。最安全的方法是, 使用 ofNullable() 來優雅地處理 null

Optional 對象操作

當我們的流管道生成了 Optional 對象,下面 3 個方法可使得 Optional 的後續能做更多的操作:

  • filter(Predicate):將 Predicate 應用於 Optional 中的內容並返回結果。當 Optional 不滿足 Predicate 時返回空。如果 Optional 爲空,則直接返回。

  • map(Function):如果 Optional 不爲空,應用 FunctionOptional 中的內容,並返回結果。否則直接返回 Optional.empty

  • flatMap(Function):同 map(),但是提供的映射函數將結果包裝在 Optional 對象中,因此 flatMap() 不會在最後進行任何包裝。

以上方法都不適用於數值型 Optional。一般來說,流的 filter() 會在 Predicate 返回 false 時移除流元素。而 Optional.filter() 在失敗時不會刪除 Optional,而是將其保留下來,並轉化爲空。下面請看代碼示例:

// streams/OptionalFilter.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
class OptionalFilter {
    static String[] elements = {
            "Foo", "", "Bar", "Baz", "Bingo"
    };
    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }
    static void test(String descr, Predicate<String> pred) {
        System.out.println(" ---( " + descr + " )---");
        for(int i = 0; i <= elements.length; i++) {
            System.out.println(
                    testStream()
                            .skip(i)
                            .findFirst()
                            .filter(pred));
        }
    }
    public static void main(String[] args) {
        test("true", str -> true);
        test("false", str -> false);
        test("str != \"\"", str -> str != "");
        test("str.length() == 3", str -> str.length() == 3);
        test("startsWith(\"B\")",
                str -> str.startsWith("B"));
    }
}

輸出結果:

---( true )---
Optional[Foo]
Optional[]
Optional[Bar]
Optional[Baz]
Optional[Bingo]
Optional.empty
---( false )---
Optional.empty
Optional.empty
Optional.empty
Optional.empty
Optional.empty
Optional.empty
---( str != "" )---
Optional[Foo]
Optional.empty
Optional[Bar]
Optional[Baz]
Optional[Bingo]
Optional.empty
---( str.length() == 3 )---
Optional[Foo]
Optional.empty
Optional[Bar]
Optional[Baz]
Optional.empty
Optional.empty
---( startsWith("B") )---
Optional.empty
Optional.empty
Optional[Bar]
Optional[Baz]
Optional[Bingo]
Optional.empty

即使輸出看起來像流,特別是 test() 中的 for 循環。每一次的 for 循環時重新啓動流,然後根據 for 循環的索引跳過指定個數的元素,這就是它最終在流中的每個連續元素上的結果。接下來調用 findFirst() 獲取剩餘元素中的第一個元素,結果會包裝在 Optional 中。

注意,不同於普通 for 循環,這裏的索引值範圍並不是 i < elements.length, 而是 i <= elements.length。所以最後一個元素實際上超出了流。方便的是,這將自動成爲 Optional.empty,你可以在每一個測試的結尾中看到。

map() 一樣 , Optional.map() 應用於函數。它僅在 Optional 不爲空時才應用映射函數,並將 Optional 的內容提取到映射函數。代碼示例:

// streams/OptionalMap.java
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Stream;

class OptionalMap {
    static String[] elements = {"12", "", "23", "45"};

    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }

    static void test(String descr, Function<String, String> func) {
        System.out.println(" ---( " + descr + " )---");
        for (int i = 0; i <= elements.length; i++) {
            System.out.println(
                    testStream()
                            .skip(i)
                            .findFirst() // Produces an Optional
                            .map(func));
        }
    }

    public static void main(String[] args) {
        // If Optional is not empty, map() first extracts
        // the contents which it then passes
        // to the function:
        test("Add brackets", s -> "[" + s + "]");
        test("Increment", s -> {
            try {
                return Integer.parseInt(s) + 1 + "";
            } catch (NumberFormatException e) {
                return s;
            }
        });
        test("Replace", s -> s.replace("2", "9"));
        test("Take last digit", s -> s.length() > 0 ?
                s.charAt(s.length() - 1) + "" : s);
    }
    // After the function is finished, map() wraps the
    // result in an Optional before returning it:
}

輸出結果:

---( Add brackets )---
Optional[[12]]
Optional[[]]
Optional[[23]]
Optional[[45]]
Optional.empty
---( Increment )---
Optional[13]
Optional[]
Optional[24]
Optional[46]
Optional.empty
---( Replace )---
Optional[19]
Optional[]
Optional[93]
Optional[45]
Optional.empty
---( Take last digit )---
Optional[2]
Optional[]
Optional[3]
Optional[5]
Optional.empty

映射函數的返回結果會自動包裝成爲 OptionalOptional.empty 會被直接跳過。

OptionalflatMap() 應用於已生成 Optional 的映射函數,所以 flatMap() 不會像 map() 那樣將結果封裝在 Optional 中。代碼示例:

// streams/OptionalFlatMap.java
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

class OptionalFlatMap {
    static String[] elements = {"12", "", "23", "45"};

    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }

    static void test(String descr,
                     Function<String, Optional<String>> func) {
        System.out.println(" ---( " + descr + " )---");
        for (int i = 0; i <= elements.length; i++) {
            System.out.println(
                    testStream()
                            .skip(i)
                            .findFirst()
                            .flatMap(func));
        }
    }

    public static void main(String[] args) {
        test("Add brackets",
                s -> Optional.of("[" + s + "]"));
        test("Increment", s -> {
            try {
                return Optional.of(
                        Integer.parseInt(s) + 1 + "");
            } catch (NumberFormatException e) {
                return Optional.of(s);
            }
        });
        test("Replace",
                s -> Optional.of(s.replace("2", "9")));
        test("Take last digit",
                s -> Optional.of(s.length() > 0 ?
                        s.charAt(s.length() - 1) + ""
                        : s));
    }
}

輸出結果:

---( Add brackets )---
Optional[[12]]
Optional[[]]
Optional[[23]]
Optional[[45]]
Optional.empty
 ---( Increment )---
Optional[13]
Optional[]
Optional[24]
Optional[46]
Optional.empty
 ---( Replace )---
Optional[19]
Optional[]
Optional[93]
Optional[45]
Optional.empty
 ---( Take last digit )---
Optional[2]
Optional[]
Optional[3]
Optional[5]
Optional.empty

map()flatMap() 將提取非空 Optional 的內容並將其應用在映射函數。唯一的區別就是 flatMap() 不會把結果包裝在 Optional 中,因爲映射函數已經被包裝過了。在如上示例中,我們已經在每一個映射函數中顯式地完成了包裝,但是很顯然 Optional.flatMap() 是爲那些自己已經生成 Optional 的函數而設計的。

Optional 流

假設你的生成器可能產生 null 值,那麼當用它來創建流時,你會自然地想到用 Optional 來包裝元素。如下是它的樣子,代碼示例:

// streams/Signal.java
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
public class Signal {
    private final String msg;
    public Signal(String msg) { this.msg = msg; }
    public String getMsg() { return msg; }
    @Override
    public String toString() {
        return "Signal(" + msg + ")";
    }
    static Random rand = new Random(47);
    public static Signal morse() {
        switch(rand.nextInt(4)) {
            case 1: return new Signal("dot");
            case 2: return new Signal("dash");
            default: return null;
        }
    }
    public static Stream<Optional<Signal>> stream() {
        return Stream.generate(Signal::morse)
                .map(signal -> Optional.ofNullable(signal));
    }
}

當我們使用這個流的時候,必須要弄清楚如何解包 Optional。代碼示例:

// streams/StreamOfOptionals.java
import java.util.*;
import java.util.stream.*;
public class StreamOfOptionals {
    public static void main(String[] args) {
        Signal.stream()
                .limit(10)
                .forEach(System.out::println);
        System.out.println(" ---");
        Signal.stream()
                .limit(10)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .forEach(System.out::println);
    }
}

輸出結果:

Optional[Signal(dash)]
Optional[Signal(dot)]
Optional[Signal(dash)]
Optional.empty
Optional.empty
Optional[Signal(dash)]
Optional.empty
Optional[Signal(dot)]
Optional[Signal(dash)]
Optional[Signal(dash)]
---
Signal(dot)
Signal(dot)
Signal(dash)
Signal(dash)

在這裏,我們使用 filter() 來保留那些非空 Optional,然後在 map() 中使用 get() 獲取元素。由於每種情況都需要定義“空值”的含義,所以通常我們要爲每個應用程序採用不同的方法。

終端操作

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

數組

  • toArray():將流轉換成適當類型的數組。
  • toArray(generator):在特殊情況下,生成自定義類型的數組。

當我們需要得到數組類型的數據以便於後續操作時,上面的方法就很有用。假設我們需要複用流產生的隨機數時,就可以這麼使用。代碼示例:

// streams/RandInts.java
package streams;
import java.util.*;
import java.util.stream.*;
public class RandInts {
    private static int[] rints = new Random(47).ints(0, 1000).limit(100).toArray();
    public static IntStream rands() {
        return Arrays.stream(rints);
    }
}

上例將100個數值範圍在 0 到 1000 之間的隨機數流轉換成爲數組並將其存儲在 rints 中。這樣一來,每次調用 rands() 的時候可以重複獲取相同的整數流。

循環

  • forEach(Consumer)常見如 System.out::println 作爲 Consumer 函數。
  • forEachOrdered(Consumer): 保證 forEach 按照原始流順序操作。

第一種形式:無序操作,僅在引入並行流時纔有意義。在 併發編程 章節之前我們不會深入研究這個問題。這裏簡單介紹下 parallel():可實現多處理器並行操作。實現原理爲將流分割爲多個(通常數目爲 CPU 核心數)並在不同處理器上分別執行操作。因爲我們採用的是內部迭代,而不是外部迭代,所以這是可能實現的。

parallel() 看似簡單,實則棘手。更多內容將在稍後的 併發編程 章節中學習。

下例引入 parallel() 來幫助理解 forEachOrdered(Consumer) 的作用和使用場景。代碼示例:

// streams/ForEach.java
import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class ForEach {
    static final int SZ = 14;
    public static void main(String[] args) {
        rands().limit(SZ)
                .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        rands().limit(SZ)
                .parallel()
                .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        rands().limit(SZ)
                .parallel()
                .forEachOrdered(n -> System.out.format("%d ", n));
    }
}

輸出結果:

258 555 693 861 961 429 868 200 522 207 288 128 551 589
551 861 429 589 200 522 555 693 258 128 868 288 961 207
258 555 693 861 961 429 868 200 522 207 288 128 551 589

爲了方便測試不同大小的數組,我們抽離出了 SZ 變量。結果很有趣:在第一個流中,未使用 parallel() ,所以 rands() 按照元素迭代出現的順序顯示結果;在第二個流中,引入parallel() ,即便流很小,輸出的結果順序也和前面不一樣。這是由於多處理器並行操作的原因。多次運行測試,結果均不同。多處理器並行操作帶來的非確定性因素造成了這樣的結果。

在最後一個流中,同時使用了 parallel()forEachOrdered() 來強制保持原始流順序。因此,對非並行流使用 forEachOrdered() 是沒有任何影響的。

集合

  • collect(Collector):使用 Collector 收集流元素到結果集合中。
  • collect(Supplier, BiConsumer, BiConsumer):同上,第一個參數 Supplier 創建了一個新結果集合,第二個參數 BiConsumer 將下一個元素包含到結果中,第三個參數 BiConsumer 用於將兩個值組合起來。

在這裏我們只是簡單介紹了幾個 Collectors 的運用示例。實際上,它還有一些非常複雜的操作實現,可通過查看 java.util.stream.Collectors 的 API 文檔瞭解。例如,我們可以將元素收集到任意一種特定的集合中。

假設我們現在爲了保證元素有序,將元素存儲在 TreeSet 中。Collectors 裏面沒有特定的 toTreeSet(),但是我們可以通過將集合的構造函數引用傳遞給 Collectors.toCollection(),從而構建任何類型的集合。下面我們來將一個文件中的單詞收集到 TreeSet 集合中。代碼示例:

// streams/TreeSetOfWords.java
import java.util.*;
import java.nio.file.*;
import java.util.stream.*;
public class TreeSetOfWords {
    public static void
    main(String[] args) throws Exception {
        Set<String> words2 =
                Files.lines(Paths.get("TreeSetOfWords.java"))
                        .flatMap(s -> Arrays.stream(s.split("\\W+")))
                        .filter(s -> !s.matches("\\d+")) // No numbers
                        .map(String::trim)
                        .filter(s -> s.length() > 2)
                        .limit(100)
                        .collect(Collectors.toCollection(TreeSet::new));
        System.out.println(words2);
    }
}

輸出結果:

[Arrays, Collectors, Exception, Files, Output, Paths,
Set, String, System, TreeSet, TreeSetOfWords, args,
class, collect, file, filter, flatMap, get, import,
java, length, limit, lines, main, map, matches, new,
nio, numbers, out, println, public, split, static,
stream, streams, throws, toCollection, trim, util,
void, words2]

Files.lines() 打開 Path 並將其轉換成爲行流。下一行代碼將匹配一個或多個非單詞字符(\\w+)行進行分割,然後使用 Arrays.stream() 將其轉化成爲流,並將結果展平映射成爲單詞流。使用 matches(\\d+) 查找並移除全數字字符串(注意,words2 是通過的)。接下來我們使用 String.trim() 去除單詞兩邊的空白,filter() 過濾所有長度小於3的單詞,緊接着只獲取100個單詞,最後將其保存到 TreeSet 中。

我們也可以在流中生成 Map。代碼示例:

// streams/MapCollector.java
import java.util.*;
import java.util.stream.*;
class Pair {
    public final Character c;
    public final Integer i;
    Pair(Character c, Integer i) {
        this.c = c;
        this.i = i;
    }
    public Character getC() { return c; }
    public Integer getI() { return i; }
    @Override
    public String toString() {
        return "Pair(" + c + ", " + i + ")";
    }
}
class RandomPair {
    Random rand = new Random(47);
    // An infinite iterator of random capital letters:
    Iterator<Character> capChars = rand.ints(65,91)
            .mapToObj(i -> (char)i)
            .iterator();
    public Stream<Pair> stream() {
        return rand.ints(100, 1000).distinct()
                .mapToObj(i -> new Pair(capChars.next(), i));
    }
}
public class MapCollector {
    public static void main(String[] args) {
        Map<Integer, Character> map =
                new RandomPair().stream()
                        .limit(8)
                        .collect(
                                Collectors.toMap(Pair::getI, Pair::getC));
        System.out.println(map);
    }
}

輸出結果:

{688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N}

Pair 只是一個基礎的數據對象。RandomPair 創建了隨機生成的 Pair 對象流。在 Java 中,我們不能直接以某種方式組合兩個流。所以這裏創建了一個整數流,並且使用 mapToObj() 將其轉化成爲 Pair 流。 capChars 隨機生成的大寫字母迭代器從流開始,然後 iterator() 允許我們在 stream() 中使用它。就我所知,這是組合多個流以生成新的對象流的唯一方法。

在這裏,我們只使用最簡單形式的 Collectors.toMap(),這個方法值需要一個可以從流中獲取鍵值對的函數。還有其他重載形式,其中一種形式是在遇到鍵值衝突時,需要一個函數來處理這種情況。

大多數情況下,java.util.stream.Collectors 中預設的 Collector 就能滿足我們的要求。除此之外,你還可以使用第二種形式的 collect()。 我把它留作更高級的練習,下例給出基本用法:

// streams/SpecialCollector.java
import java.util.*;
import java.util.stream.*;
public class SpecialCollector {
    public static void main(String[] args) throws Exception {
        ArrayList<String> words =
                FileToWords.stream("Cheese.dat")
                        .collect(ArrayList::new,
                                ArrayList::add,
                                ArrayList::addAll);
        words.stream()
                .filter(s -> s.equals("cheese"))
                .forEach(System.out::println);
    }
}

輸出結果:

cheese
cheese

在這裏, ArrayList 的方法已經執行了你所需要的操作,但是似乎更有可能的是,如果你必須使用這種形式的 collect(),則必須自己創建特殊的定義。

組合

  • reduce(BinaryOperator):使用 BinaryOperator 來組合所有流中的元素。因爲流可能爲空,其返回值爲 Optional
  • reduce(identity, BinaryOperator):功能同上,但是使用 identity 作爲其組合的初始值。因此如果流爲空,identity 就是結果。
  • reduce(identity, BiFunction, BinaryOperator):更復雜的使用形式(暫不介紹),這裏把它包含在內,因爲它可以提高效率。通常,我們可以顯式地組合 map()reduce() 來更簡單的表達它。

下面來看下 reduce 的代碼示例:

// streams/Reduce.java
import java.util.*;
import java.util.stream.*;
class Frobnitz {
    int size;
    Frobnitz(int sz) { size = sz; }
    @Override
    public String toString() {
        return "Frobnitz(" + size + ")";
    }
    // Generator:
    static Random rand = new Random(47);
    static final int BOUND = 100;
    static Frobnitz supply() {
        return new Frobnitz(rand.nextInt(BOUND));
    }
}
public class Reduce {
    public static void main(String[] args) {
        Stream.generate(Frobnitz::supply)
                .limit(10)
                .peek(System.out::println)
                .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
                .ifPresent(System.out::println);
    }
}

輸出結果:

Frobnitz(58)
Frobnitz(55)
Frobnitz(93)
Frobnitz(61)
Frobnitz(61)
Frobnitz(29)
Frobnitz(68)
Frobnitz(0)
Frobnitz(22)
Frobnitz(7)
Frobnitz(29)

Frobnitz 包含了一個名爲 supply() 的生成器;因爲這個方法對於 Supplier<Frobnitz> 是簽名兼容的,我們可以將其方法引用傳遞給 Stream.generate()(這種簽名兼容性被稱作結構一致性)。無“初始值”的 reduce()方法返回值是 Optional 類型。Optional.ifPresent() 只有在結果非空的時候纔會調用 Consumer<Frobnitz>println 方法可以被調用是因爲 Frobnitz 可以通過 toString() 方法轉換成 String)。

Lambda 表達式中的第一個參數 fr0 是上一次調用 reduce() 的結果。而第二個參數 fr1 是從流傳遞過來的值。

reduce() 中的 Lambda 表達式使用了三元表達式來獲取結果,當其長度小於 50 的時候獲取 fr0 否則獲取序列中的下一個值 fr1。當取得第一個長度小於 50 的 Frobnitz,只要得到結果就會忽略其他。這是個非常奇怪的約束, 也確實讓我們對 reduce() 有了更多的瞭解。

匹配

  • allMatch(Predicate) :如果流的每個元素根據提供的 Predicate 都返回 true 時,結果返回爲 true。在第一個 false 時,則停止執行計算。
  • anyMatch(Predicate):如果流中的任意一個元素根據提供的 Predicate 返回 true 時,結果返回爲 true。在第一個 false 是停止執行計算。
  • noneMatch(Predicate):如果流的每個元素根據提供的 Predicate 都返回 false 時,結果返回爲 true。在第一個 true 時停止執行計算。

我們已經在 Prime.java 中看到了 noneMatch() 的示例;allMatch()anyMatch() 的用法基本上是等同的。下面我們來探究一下短路行爲。爲了消除冗餘代碼,我們創建了 show()。首先我們必須知道如何統一地描述這三個匹配器的操作,然後再將其轉換爲 Matcher 接口。代碼示例:

// streams/Matching.java
// Demonstrates short-circuiting of *Match() operations
import java.util.stream.*;
import java.util.function.*;
import static streams.RandInts.*;

interface Matcher extends BiPredicate<Stream<Integer>, Predicate<Integer>> {}
        
public class Matching {
    static void show(Matcher match, int val) {
        System.out.println(
                match.test(
                        IntStream.rangeClosed(1, 9)
                                .boxed()
                                .peek(n -> System.out.format("%d ", n)),
                        n -> n < val));
    }
    public static void main(String[] args) {
        show(Stream::allMatch, 10);
        show(Stream::allMatch, 4);
        show(Stream::anyMatch, 2);
        show(Stream::anyMatch, 0);
        show(Stream::noneMatch, 5);
        show(Stream::noneMatch, 0);
    }
}

輸出結果:

1 2 3 4 5 6 7 8 9 true
1 2 3 4 false
1 true
1 2 3 4 5 6 7 8 9 false
1 false
1 2 3 4 5 6 7 8 9 true

BiPredicate 是一個二元謂詞,它只能接受兩個參數且只返回 true 或者 false。它的第一個參數是我們要測試的流,第二個參數是一個謂詞 PredicateMatcher 適用於所有的 Stream::*Match 方法,所以我們可以傳遞每一個到 show() 中。match.test() 的調用會被轉換成 Stream::*Match 函數的調用。

show() 獲取兩個參數,Matcher 匹配器和用於表示謂詞測試 n < val 中最大值的 val。這個方法生成一個1-9之間的整數流。peek() 是用於向我們展示測試在短路之前的情況。從輸出中可以看到每次都發生了短路。

查找

  • findFirst():返回第一個流元素的 Optional,如果流爲空返回 Optional.empty
  • findAny(:返回含有任意流元素的 Optional,如果流爲空返回 Optional.empty

代碼示例:

// streams/SelectElement.java
import java.util.*;
import java.util.stream.*;
import static streams.RandInts.*;
public class SelectElement {
    public static void main(String[] args) {
        System.out.println(rands().findFirst().getAsInt());
        System.out.println(
                rands().parallel().findFirst().getAsInt());
        System.out.println(rands().findAny().getAsInt());
        System.out.println(
                rands().parallel().findAny().getAsInt());
    }
}

輸出結果:

258
258
258
242

findFirst() 無論流是否爲並行化的,總是會選擇流中的第一個元素。對於非並行流,findAny()會選擇流中的第一個元素(即使從定義上來看是選擇任意元素)。在這個例子中,我們使用 parallel() 來並行流從而引入 findAny() 選擇非第一個流元素的可能性。

如果必須選擇流中最後一個元素,那就使用 reduce()。代碼示例:

// streams/LastElement.java
import java.util.*;
import java.util.stream.*;
public class LastElement {
    public static void main(String[] args) {
        OptionalInt last = IntStream.range(10, 20)
                .reduce((n1, n2) -> n2);
        System.out.println(last.orElse(-1));
        // Non-numeric object:
        Optional<String> lastobj =
                Stream.of("one", "two", "three")
                        .reduce((n1, n2) -> n2);
        System.out.println(
                lastobj.orElse("Nothing there!"));
    }
}

輸出結果:

19
three

reduce() 的參數只是用最後一個元素替換了最後兩個元素,最終只生成最後一個元素。如果爲數字流,你必須使用相近的數字 Optional 類型( numeric optional type),否則使用 Optional 類型,就像上例中的 Optional<String>

信息

  • count():流中的元素個數。
  • max(Comparator):根據所傳入的 Comparator 所決定的“最大”元素。
  • min(Comparator):根據所傳入的 Comparator 所決定的“最小”元素。

String 類型有預設的 Comparator 實現。代碼示例:

// streams/Informational.java
import java.util.stream.*;
import java.util.function.*;
public class Informational {
    public static void
    main(String[] args) throws Exception {
        System.out.println(
                FileToWords.stream("Cheese.dat").count());
        System.out.println(
                FileToWords.stream("Cheese.dat")
                        .min(String.CASE_INSENSITIVE_ORDER)
                        .orElse("NONE"));
        System.out.println(
                FileToWords.stream("Cheese.dat")
                        .max(String.CASE_INSENSITIVE_ORDER)
                        .orElse("NONE"));
    }
}

輸出結果:

32
a
you

min()max() 的返回類型爲 Optional,這需要我們使用 orElse()來解包。

數字流信息

  • average() :求取流元素平均值。
  • max()min():數值流操作無需 Comparator
  • sum():對所有流元素進行求和。
  • summaryStatistics():生成可能有用的數據。目前並不太清楚這個方法存在的必要性,因爲我們其實可以用更直接的方法獲得需要的數據。
// streams/NumericStreamInfo.java
import java.util.stream.*;
import static streams.RandInts.*;
public class NumericStreamInfo {
    public static void main(String[] args) {
        System.out.println(rands().average().getAsDouble());
        System.out.println(rands().max().getAsInt());
        System.out.println(rands().min().getAsInt());
        System.out.println(rands().sum());
        System.out.println(rands().summaryStatistics());
    }
}

輸出結果:

507.94
998
8
50794
IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998}

上例操作對於 LongStreamDoubleStream 同樣適用。

本章小結

流式操作改變並極大地提升了 Java 語言的可編程性,並可能極大地阻止了 Java 編程人員向諸如 Scala 這種函數式語言的流轉。在本書的剩餘部分,我們將盡可能地使用流。

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