Java Stream和Collection比較:何時以及如何從Java API返回Stream而不是集合Collection? - TomaszKiełbowicz
向您展示一些可以非常方便地使用Java Stream流的場景以及如何使用它們的示例。
本文基於標準Java庫java.util.stream。它既與反應流無關,也與諸如Vavr之類的其他流實現無關。另外,我將不介紹諸如並行執行之類的流的高階細節。
首先,讓我們簡要討論與集合相比獨特的流功能。儘管存在一些相似之處,但差異是很大的,您不應將流僅視為庫中的另一種集合。
根據java.util.stream 的文件,最重要的功能是:
- 沒有儲存空間,可能是無限制的 -集合是現成的資料結構,而流表示產生資料的能力,通常在建立流時甚至不存在。由於不儲存流中的資料,因此我們可以建立幾乎不確定的流,或者可以更實際地對其重新措辭,我們可以讓消費者決定要從流中讀取多少個元素,從生產者的角度來看,它可能是不確定的(例如new Random().ints())。
- 懶惰載入 —在定義流時暫停許多操作(例如過濾,對映),並且僅在使用者決定使用流中的資料時才執行
- 本質上是實用的 -由於您已經具有使用流的經驗,因此您可能會注意到處理流中的資料是為每個步驟(例如過濾器或對映)建立新流,而不是修改源資料
- 消耗性 -您只能讀取一次流,然後與可以多次讀取的集合不同,它變為“消耗性”
現在讓我們看看我們可以用流解決什麼問題。
處理大量資料
假設,我們必須將資料從外部服務複製到我們的資料庫中。要複製的資料量可以任意大。我們無法獲取所有資料,無法將其儲存在一個集合中,然後儲存在資料庫中,因為這可能會耗盡堆記憶體。我們必須分批處理資料,並設計外部服務客戶端和資料庫儲存之間的介面。由於流不儲存日期,因此可以使用它安全地處理所需的資料量。
在示例(及以下所有示例)中,我們將使用java.util.stream.Stream介面的靜態方法來構建流。用Java構建流的最強大,最靈活的方法是實現Spliterator介面,然後使用StreamSupport類將其包裝為流。但是,正如我們所看到的,Stream在許多情況下,介面中的靜態工廠方法就足夠了。
假定一個簡單的API從支援分頁的外部服務(例如,REST服務,資料庫)中獲取資料。該API最多可limit從提取專案offset。迭代地使用API,我們可以根據需要獲取儘可能多的資料。
interface ExternalService { List<String> fetch(int offset, int limit); } |
現在,我們可以使用API提供資料流,並將API的使用者與分頁API隔離開:
class Service<T> { private final ExternalService<T> externalService; public Stream<T> stream(int size, int batchSize) { var cursor = new Cursor(); return Stream .generate(() -> next(cursor, size, batchSize)) .takeWhile(not(List::isEmpty)) .flatMap(List::stream); } private List<T> next(Cursor cursor, int size, int batchSize) { var fetchSize = Math.min(size - cursor.offset, batchSize); var result = externalService.fetch(cursor.offset, fetchSize); cursor.inc(result.size()); return result; } } |
Cursor 握有當前偏移量offset:
private static class Cursor { private int offset; void inc(int by) { offset += by; } } |
我們使用Stream.generate()方法構建無限流,其中每個元素由流提供者建立。流元素是從REST API獲取的頁面List<T>。將為每個流建立Cursor類的例項,以跟蹤獲取的元素的進度。
Stream.takeWhile()方法用於檢測的最後一頁,最後返回的資料流T,而不是List<T>。
我們使用flatMap扁平化流。儘管在某些情況下,保留批處理(例如將整個頁面儲存在一個事務中)可能很有用。
現在,我們可以使用Service.stream(size, batchSize)來檢索任意長流,而無需任何分頁API的知識(我們決定公開batchSize引數,但這是一個設計決策)。在任何時間點,記憶體消耗都受到批處理大小的限制。使用者可以一一處理資料,將其儲存在資料庫中,或者再次進行批處理(批處理大小可能不同)。
快速訪問(不完整)資料
假設我們有一個耗時的操作,必須對資料的每個元素執行該操作,並且計算要花費時間t。對於n元素,使用者必須等待t * n才能接收到計算結果。例如,如果使用者正在等待帶有計算結果的表,則可能是一個問題。我們希望在顯示第一結果時立即顯示它們,而不是等待所有結果的計算並立即提交表。
public class Producer1 { private Stream<String> buildStream() { return Stream.of("a", "b", "c"); } private String expensiveStringDoubler(String input) { return input + input; } public Stream<String> stream() { return buildStream().map(this::expensiveComputation); } } |
消費者:
stream().forEach(System.out::println) |
輸出:
Processing of: a aa Processing of: b … |
輸出:
Processing of: a aa Processing of: b … |
如我們所見,在開始處理下一個元素之前,使用者可以使用第一個元素“ aa ”的處理結果,但是計算仍然是流的生產者責任。換句話說,消費者決定何時以及是否應該執行計算,但是生產者仍然負責如何執行計算。
您可能會認為這很容易,並且不需要流。當然,您是對的,讓我們看一下:
public class Producer1Classic { public List<String> data() { return List.of("a", "b", "c", "d", "e", "f"); } public String expensiveStringDoubler(String input) { return input + input; } } |
消費者:
var producer = new Producer1Classic(); for (String element : producer.data()) { System.out.println(producer.expensiveComputation(element)); } |
同樣的效果,但是實際上我們已經重新發明了輪子,我們的實現模仿了stream的祖先- Iterator並且我們失去了stream的API的優勢。
避免過早計算
再次假設我們要對每個流元素執行耗時的操作。在某些情況下,API的使用者無法提前說出需要多少資料。例如:
- 使用者取消了資料載入
- 在資料處理過程中發生錯誤,無需處理其餘資料
- 消費者讀取資料直到滿足條件,例如第一個正值
由於流的惰性,在這種情況下可以避免一些計算。
private Stream<Double> buildStream() { return new Random().doubles().boxed(); } private Double expensiveComputation(Double input) { return input / 2; } public Stream<Double> stream() { return buildStream().map(this::expensiveComputation); } |
消費者:
stream().peek(System.out::println).filter(value -> value > 0.4).findFirst(); |
在該示例中,使用者讀取資料,直到該值大於0.4。生產者並不瞭解消費者的這種邏輯,但它只計算必要的專案。邏輯(例如條件)可以在使用者端獨立更改。
API易於使用
使用流而不是自定義API設計還有另一個原因。流是標準庫的一部分,併為許多開發人員所熟知。在我們的API中使用流使其他開發人員更容易使用該API。
其他注意事項
錯誤處理
傳統的錯誤處理不適用於Streams。由於實際處理將推遲到需要時進行,因此構造流時不會引發異常。基本上,我們有兩個選擇:
- 引發RuntimeException-終止方法(例如forEach)將引發異常
- 將元素包裝到一個物件中,該物件表示正在處理的元素的當前狀態,例如TryVavr庫中的特殊類(部落格中的詳細資訊)
資源管理
有時我們必須使用一種資源來提供流資料(例如,外部服務中的會話),並且我們想在流處理完成時將其釋放。幸運的是,流實現了Autoclosable介面,我們可以在try-with-resources語句中使用流,從而使資源管理變得非常容易。我們要做的就是使用onClose方法在流中註冊一個鉤子。當流關閉時,該掛鉤將自動被呼叫。
private Stream<Double> buildStream() { return new Random().doubles().boxed(); } private Double expensiveComputation(Double input) { if (input > 0.8) throw new RuntimeException("Data processing exception"); return input / 2; } public Stream<Double> stream() { return buildStream().map(this::expensiveComputation).onClose(()-> System.out.println("Releasing resources…")); } |
消費者:
try (Stream<Double> stream = stream()){ stream.forEach(System.out::println); } |
輸出:
0.2264004802916616 0.32777949557515484 Releasing resources… Exception in thread “main” java.lang.RuntimeException: Data processing exception |
在該示例中,當發生資料處理異常時,流將通過try-with-resources語句自動關閉,並呼叫已註冊的處理程式。在示例輸出中,我們可以看到Releasing resources…處理程式列印的訊息。
總結
- 流不是集合。
- 流可以幫助我們解決以下問題:*處理大量資料*快速訪問(不完整的)資料*避免過早計算
- 構建流並不難。
- 我們必須注意錯誤處理。
- 支援資源管理。
相關文章
- Java8 Lambda 之 Collection StreamJava
- 【Java】Collection.sort以及比較器ComparatorJava
- Java集合-CollectionJava
- Java:Collection集合、泛型Java泛型
- Collection如何轉成stream以及Spliterator對其操作的實現
- Java 的 Collection 與 List 集合Java
- Java Collection介面 ArrayList集合(容器)Java
- Java Collection集合面試題Java面試題
- Java 10中Stream API不可變集合JavaAPI
- Java 8 Stream API 轉換到 Kotlin 集合APIJavaAPIKotlin
- Java 不可變集合 Stream流以及方法引用Java
- Android基礎之Java集合框架CollectionAndroidJava框架
- 【Java集合】單列集合Collection常用方法詳解Java
- java stream()流對兩個集合進行比對Java
- Java Stream API groupingBy()介紹JavaAPI
- Java8中的 lambda 和Stream APIJavaAPI
- java.util.Collection集合方法:Collections.BinarySearch 方法Java
- [Java基礎]collectionJava
- Java™ 教程(Collection介面)Java
- java .stream(). 使用介紹 Streams APIJavaAPI
- Java8中的Stream APIJavaAPI
- Java8 Stream常用API整理JavaAPI
- Java8的Stream API使用JavaAPI
- Java8新特性--Stream APIJavaAPI
- java8 Stream APi 入門JavaAPI
- Java StreamJava
- Java集合 Collection、Set、Map、泛型 簡要筆記Java泛型筆記
- Collection集合、List集合及其方法
- Java-stream(1) Stream基本概念 & Stream介面Java
- 7個Java Stream API面試題JavaAPI面試題
- Java 8 Stream Api 中的 peek 操作JavaAPI
- Java8 - Stream API快速入門JavaAPI
- Java Lambda StreamJava
- [Java]Stream用法Java
- Java 8 StreamJava
- Collection集合的遍歷
- Java Stream API:實現 Kruskal 演算法JavaAPI演算法
- Go語言實現的Java Stream APIGoJavaAPI