Java8新特性Lambda表達式&Stream流&方法引用最全集錦

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

流是一系列與特定存儲機制無關的元素——實際上,流並無「存儲」之說。git

利用流,無需迭代集合中的元素,就能夠提取和操做它們。這些管道一般被組合在一塊兒,在流上造成一條操做管道。正則表達式

大多數狀況下,將對象存儲在集合是爲了處理他們,所以你將會發現編程焦點從集合轉移到了流。流的一個核心好處是,它使得程序更加短小而且更易理解。當 Lambda 表達式和方法引用和流一塊兒使用的時候會讓人感受自成一體。流使得 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::printlnapi

注意 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);
    }
}
複製代碼

輸出結果:markdown

[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() 產生一個流。示例:

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
  • 經過調用字符串的 split()來獲取元素用於定義變量 w
  • 爲了從 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() 做爲過濾器函數,用於檢測質數。

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,而是將其保留下來,並轉化爲空。

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 一樣適用。

相關文章
相關標籤/搜索