1. 程式人生 > >Java8 新特性 —— Stream 流式程式設計

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


> 本文部分摘自 On Java 8
## 流概述 集合優化了物件的儲存,大多數情況下,我們將物件儲存在集合是為了處理他們。使用流可以幫助我們處理物件,無需迭代集合中的元素,即可直接提取和操作元素,並添加了很多便利的操作,例如查詢、過濾、分組、排序等一系列操作。 流的一個核心好處是:它使得程式更加短小並且易於理解,當結合 Lambda 表示式和方法引用時,會讓人感覺自成一體。總而言之,流就是一種高效且易於使用的處理資料的方式。 觀察下面的例子: ```java public class Randoms { public static void main(String[] args) { new Random(47) // 建立 Random 物件,並給一個種子 .ints(5, 20) // 產生一個限定了邊界的隨機整數流 .distinct() // 使流中的整數不重複 .limit(7) // 取前7個元素 .sorted() // 排序 .forEach(System.out::println); // 根據傳遞給它的函式對流中每個物件執行操作 } } ``` 通過上面的示例,我們可以發現流有如下特點: 1. 流本身不儲存元素,並且不會改變源物件,相反,它會返回一個持有結果的新流 2. 流可以在不使用賦值或可變資料的情況下對有狀態的系統建模 3. 流是一種宣告式程式設計風格,它宣告想要做什麼,而非指明如何做 4. 流的迭代過稱為內部迭代,你看不到迭代過程,可讀性更強 5. 流是懶載入的,它會等到需要時才執行
## 流建立 建立流的方式有很多,下面逐個介紹: #### 1. Stream.of() 通過 `Stream.of()` 可以很容易地將一組元素轉化為流 ```java Stream.of(new Bubble(1), new Bubble(2), new Bubble(3)).forEach(System.out::println); Stream.of("a", "b", "c", "d", "e", "f").forEach(System.out::print); Stream.of(3.14159, 2.718, 1.618).forEach(System.out::println); ``` #### 2. stream() 每個集合也可以通過呼叫 `stream()` 方法來產生一個流 ```java List list = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3)); list.stream().forEach(System.out::print); Set set = new HashSet<>(Arrays.asList("a", "b", "c", "d", "e", "f")); set.stream().forEach(System.out::print); ``` #### 3. Stream.generate() 使用 `Stream.generate()` 搭配 `Supplier` 生成 T 型別的流 ```java Stream.generate(Math::random).limit(10).forEach(System.out::print); ``` #### 4. Stream.iterate() `Stream.iterate()` 產生的流的第一個元素是種子,然後把種子傳遞給方法,方法的執行結果被新增到流,並作為下次呼叫 `iterate()` 的第一個引數 ```java Stream.iterate(0, n -> n + 1).limit(10).forEach(System.out::print) ``` 使用 `Stream.generate()` 和 `Stream.iterate()` 生成的無限流一定要用 `limit()` 截斷 #### 5. Stream.builder() 使用建造者模式建立一個 `builder` 物件,然後將建立流所需的多個資訊傳遞給它,最後 `builder` 物件執行建立流的操作 ```java Stream.Builder builder = Stream.builder(); builder.add("a"); builder.add("b"); ... builder.build(); // 建立流 // builder.add("c") // 呼叫 build() 方法後繼續新增元素會產生異常 ``` #### 6. Arrays.stream() Arrays 類中有一個名為 `stream()` 的靜態方法用於把陣列轉換成流 ```java Arrays.stream(new double[] {3.14159, 2.718, 1.618}).forEach(System.out::print); Arrays.stream(new int[] {1, 3, 5}).forEach(System.out::print); Arrays.stream(new long[] {11, 22, 44, 66}).forEach(System.out::print); // 選擇一個子域 Arrays.stream(new int[] {1, 3, 5, 7, 15, 28, 37}, 3, 6).forEach(System.out::print); ``` 最後一次 `stream()` 的呼叫有兩個額外的引數,第一個引數告訴 `stream()` 從陣列的哪個位置開始選擇元素,第二個引數告知在哪裡停止 #### 7. IntStream.range() `IntStream` 類提供 `range()` 方法用於生成整型序列的流,編寫迴圈時,這個方法會更加便利 ```java IntStream.range(10, 20).sum(); // 求得 10 - 20 的序列和 IntStream.range(10, 20).forEach(System.out::print); // 迴圈輸出 10 - 20 ``` #### 8. 隨機數流 Random 類被一組生成流的方式增強了,可以生成一組隨機數流 ```java Random rand = new Random(47); // 產生一個隨機流 rand.ints().boxed(); // 控制上限和下限 rand.ints(10, 20).boxed(); // 控制流的大小 rand.ints(2).boxed(); // 控制流的大小和界限 rand.ints(3, 3, 9).boxed(); ``` Random 類除了能生成基本型別 int,long,double 的流,使用 `boxed()` 操作會自動把基本型別包裝為對應的裝箱型別 #### 9. 正則表示式 Java8 在 `java.util.regex.Pattern` 中新增了一個方法 `splitAsStream()`,這個方法可以根據傳入的公式將字元序列轉化為流 ```java Pattern.compile("[.,?]+").splitAsStream("a,b,c,d,e").forEach(System.out::print); ```
## 中間操作 中間操作具體包括去重、過濾、對映等操作,作用於從流中獲取的每一個物件,並返回一個新的流物件。 #### 1. 跟蹤和除錯 `peek()` 操作的目的是幫助除錯,它允許你無修改地檢視流中的元素 ```java Stream.of("a b c d e".split(" ")).map(w -> w + " ").peek(System.out::print); ``` #### 2. 流元素排序 `sorted()` 可以幫助我們實現對流元素的排序,如果不使用預設的自然排序,則需要傳入一個比較器,也可以把 Lambda 函式作為引數傳遞給 `sorted()` ```java Stream.of("a b c d e".split(" ")).sorted(Comparator.reverseOrder()) .map(w ->
w + " ").peek(System.out::print); ``` #### 3. 移除元素 `distinct()` 可用於消除流中的重複元素 ```java new Random(47).ints(5, 20).distinct().limit(7).forEach(System.out::println); ``` `filter(Predicate)` 將元素傳遞給過濾函式,若結果為 true,則保留元素 ```java // 檢測質數 Stream.iterate(2, n -> n + 1).filter(i -> i % 2 ==0) .limit(10).forEach(System.out::print) ``` #### 4. 應用函式到元素 `map(Function)` 將函式操作應用到輸入流的元素,並將返回值傳遞到輸出流 ```java Arrays.stream(new String[] {"12", "23", "34"}).map(s -> "[" + s + "]") .forEach(System.out::print) ``` 另外還有 `mapToInt(ToIntFunction)`、`mapToLong(ToLongFunction)`、`mapToDouble(ToDoubleFunction)`,操作和 `map(Function)` 相似,只是結果流為各自對應的基本型別 如果在將函式應用到元素的過程中丟擲了異常,此時會把原始元素放到輸出流 #### 5. 組合流 使用 `flatMap()` 將產生流的函式應用在每個元素上,然後將產生每個流都扁平化為元素 ```java Stream.of(1, 2, 3).flatMap(i -> Stream.of("hello" + i)).forEach(System.out::println); ``` 另外還有 `flatMapToInt(Function)`、`flatMapToLong(Function)`、`flatMapToDouble(Function)`,操作和 `flatMap()` 相似,只是結果元素為各自對應的基本型別
## Optional 類 如果在一個空流中嘗試獲取元素,結果肯定是得到一個異常。我們希望可以得到友好的提示,而不是糊你一臉 NullPointException。Optional 的出現就是為了解決臭名昭著的空指標異常 一些標準流操作返回 Optional 物件,因為它們不能保證預期結果一定存在,包括: - `findFirst()` 返回一個包含第一個元素的 Optional 物件,如果流為空則返回 Optional.empty - `findAny()` 返回包含任意元素的 Optional 物件,如果流為空則返回 Optional.empty - `max()` 和 `min()` 返回一個包含最大值或者最小值的 Optional 物件,如果流為空則返回 Optional.empty - `reduce(Function)` 將函式的返回值包裝在 Optional 中 #### 1. 便利函式 Optional 類本質上是一個容器物件,所謂容器是指:它可以儲存型別 T 的值,也可以儲存一個 null。此外,Optional 提供了許多有用的方法,可以幫助我們不用顯示地進行空值檢測: - `ifPresent()` 是否有值存在,存在放回 true,否則返回 false - `ifPresent(Consumer)` 當值存在時呼叫 Consumer,否則什麼也不做 - `orElse(otherObject)` 如果值存在則直接返回,否則生成 otherObject - `orElseGet(Supplier)` 如果值存在則直接返回,否則使用 Supplier 函式生成一個可替代物件 - `orElseThrow(Supplier)` 如果值存在則直接返回,否則使用 Supplier 函式生成一個異常 下面是對 Optional 的一個簡單應用 ```java class OptionalBasics { static void test(Optional 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.empty().findFirst()); // 生成一個空流 } } ``` #### 2.建立 Optional 當我們需要在自己的程式碼中加入 Optional 時,可以使用下面三個靜態方法: - `empty()` 生成一個空 Optional - `of(value)` 將一個非空值包裝到 Optional 裡 - `ofNullable(value)` 針對一個可能為空的值,為空時自動生成 Optional.empty,否則將值包裝在 Optional 中 #### 3. Optional 物件操作 當我們的流管道生成 Optional 物件,下面三個方法可以使得 Optional 能做更多後續操作: - `filter(Predicate)` 對 Optional 中的內容應用 Predicate 並將結果返回。如果 Optional 不滿足 Predicate,將 Optional 轉化為空 Optional 。如果 Optional 已經為空,則直接返回空 Optional - `map(Function)` 如果 Optional 不為空,應用 Function 於 Optional 中的內容,並返回結果,否則直接返回 Optional.empty - `flatMap(Function)` 一般應用於已生成 Optional 的對映函式,所以 `flatMap()` 不會像 `map()` 那樣將結果封裝在 Optional 中
## 終端操作 終端操作將獲取流的最終結果,至此我們無法再繼續往後傳遞流。可以說,終端操作總是我們在使用流時所做的最後一件事 #### 1. 陣列 當我們需要得到陣列型別的資料以便於後續操作時,可以使用下述方法產生陣列: - `toArray()` 將流轉換成適當型別的陣列 - `toArray(generetor)` 生成自定義型別的陣列 #### 2. 迴圈 常見的如 `forEach(Consumer)`,另外還有 `forEachOrdered(Consumer)`,保證按照原始流的順序操作。第二種形式僅在引入並行流時才有意義。所謂並行流是將流分割為多個,並在不同的處理器上分別執行。由於多處理器並行操作的原因,輸出的結果可能會不一樣,因此需要用到 `forEachOrdered(Consumer)` #### 3. 集合 在這裡我們只是簡單介紹一下常見的 Collectors 示例,實際上它還有一些非常複雜的實現。大多數情況下,`java.util.stream.Collectors` 中預設的 Collector 就能滿足我們的需求 - `collect(Collector)` 使用 Collector 收集流元素到結果集合中 - `collect(Supplier, BiConsumer, BiConsumer)` 第一個引數建立一個新的結果集合,第二個引數將下一個元素收集到結果集合中,第三個引數用於將兩個結果集合合併起來 #### 4. 組合 組合意味著將流中所有元素以某種方式組合為一個元素 - `reduce(BinaryOperator)` 使用 BinaryOperator 來組合所有流中的元素。因為流可能為空,其返回值為 Optional - `reduce(identity, BinaryOperator)` 功能同上,但是使用 identity 作為其組合的初始值。因此如果流為空,identity 就是結果 看一段程式碼示例: ```java Stream.generate(Math::random).limit(10) .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1).ifPresent(System.out::println); ``` 返回的結果是 Optional 型別,Lambda 表示式中的第一個引數 fr0 是 reduce 中上一次呼叫的結果,而第二個引數 fr1 是從流傳遞過來的值 #### 5. 匹配 `allMatch(Predicate)` 如果流的每個元素提供給 Predicate 都返回 true ,結果返回為 true。在第一個 false 時,則停止執行計算 `anyMatch(Predicate)` 如果流的任意一個元素提供給 Predicate 返回 true ,結果返回為 true。在第一個 true 是停止執行計算 `noneMatch(Predicate)` 如果流的每個元素提供給 Predicate 都返回 false 時,結果返回為 true。在第一個 true 時停止執行計算 #### 6. 查詢 `findFirst()` 返回第一個流元素的 Optional,如果流為空返回 Optional.empty `findAny(` 返回含有任意流元素的 Optional,如果流為空返回 Optional.empty #### 7. 資訊 `count()` 流中的元素個數 `max(Comparator)` 根據所傳入的 Comparator 所決定的最大元素 `min(Comparator)` 根據所傳入的 Comparator 所決定的最小元素 #### 8. 數字流資訊 `average()` 求取流元素平均值 `max()` 和 `min()` 數值流操作無需 Comparator `sum()` 對所有流元素進行求