[英]In which cases Stream operations should be stateful?
在stream
包的javaodoc中 ,在Parallelism
部分的末尾,我讀到:
大多數流操作接受描述用戶指定行為的參數,這些參數通常是lambda表達式。 為了保持正確的行為,這些行為參數必須是非干擾的, 並且在大多數情況下必須是無狀態的 。
在大多數情況下,我很難理解這一點。 在哪些情況下可以接受/希望進行有狀態流操作?
我的意思是,我知道這是可能的,特別是在使用順序流時,但同樣的javadoc明確指出:
除了標識為顯式非確定性的操作
findAny()
例如findAny()
,流是順序執行還是並行執行不應更改計算結果。
並且:
另請注意,嘗試從行為參數訪問可變狀態會給您在安全性和性能方面做出錯誤的選擇; [...]最好的方法是避免有狀態的行為參數完全流動操作; 通常有一種方法可以重構流管道以避免有狀態。
所以,我的問題是:在哪種情況下使用有狀態流操作是一種好習慣(而不是通過副作用工作的方法,例如forEach
)?
一個相關的問題可能是:為什么有副作用的操作,例如forEach
? 我總是最后做一個好老for
循環,以避免在我的lambda表達式的副作用。
有狀態流lambdas的示例:
collect(Collector)
: Collector
按定義是有狀態的,因為它必須收集集合(狀態)中的所有元素。 forEach(Consumer)
:根據定義, Consumer
是有狀態的,除非它是一個黑洞(無操作)。 peek(Consumer)
:根據定義, Consumer
是有狀態的,因為如果不將它存儲在某個地方(例如日志),為什么要偷看。 因此, Collector
和Consumer
是兩個lambda接口,根據定義是有狀態的。
所有其他的,例如Predicate
, Function
, UnaryOperator
, BinaryOperator
和Comparator
, 都應該是無狀態的。
在大多數情況下,我很難理解這一點。 在哪些情況下可以接受/希望進行有狀態流操作?
假設以下場景。 您有一個Stream<String>
,您需要按照自然順序列出項目,每個項目都包含訂單號。 所以,例如輸入你有: Banana
, Apple
和Grape
。 輸出應該是:
1. Apple
2. Banana
3. Grape
如何在Java Stream API中解決此任務? 很容易:
List<String> f = asList("Banana", "Apple", "Grape");
AtomicInteger number = new AtomicInteger(0);
String result = f.stream()
.sorted()
.sequential()
.map(i -> String.format("%d. %s", number.incrementAndGet(), i))
.collect(Collectors.joining("\n"));
現在,如果您查看此管道,您將看到3個有狀態操作:
sorted()
- 按定義有狀態。 請參閱Stream.sorted()
文檔:
這是一個有狀態的中間操作
map()
- 本身可以是無狀態的,但在這種情況下它不是。 要標記位置,您需要跟蹤已標記的項目數量; collect()
- 是可變的縮減操作(從docs到Stream.collect()
)。 根據定義,可變操作是有狀態的,因為它們改變(變異)共享狀態。 關於為什么sorted()
是有狀態的,存在一些爭議。 從Stream API文檔:
無狀態操作(例如過濾器和映射)在處理新元素時不保留先前看到的元素的狀態 - 每個元素都可以獨立於其他元素上的操作進行處理。 有狀態操作(例如distinct和sorted)可以在處理新元素時包含先前看到的元素的狀態。
因此,當將術語有狀態 / 無狀態應用於Stream API時,我們更多地討論流的函數處理元素,而不是關於函數處理流作為整體。
還要注意, 無狀態和確定性之間存在一些混淆。 他們不一樣。
確定性函數在給定相同參數的情況下提供相同結果
無狀態功能保留以前調用的狀態。
這些是不同的定義。 一般情況下並不依賴於彼此。 決定論是關於功能結果值,而關於功能實現的無狀態。
如有疑問,只需檢查文檔以了解具體操作。 例子:
Stream.map
映射器參數:
mapper
- 應用於每個元素的非干擾無狀態函數
這里的文檔明確說明函數必須是無狀態的。
Stream.forEach
動作參數:
action
- 對元素執行的非干擾操作
這里沒有指明動作是無狀態的,因此它可以是有狀態的。
通常,它總是明確地寫在每個方法文檔上。
無狀態函數為相同的輸入返回相同的輸出,“無論如何”。
在像Java這樣的命令式語言中創建非無狀態函數很容易。 例如
func = input -> currentTime();
如果我們使用有狀態func
執行stream.map(func)
,結果流將取決於在運行時調用func
方式; 應用程序的行為很難理解(但並不難)。
如果func
是無狀態的,則無論如何實現和執行map
, stream.map(func)
將始終生成相同的流。 這很好,也很可取。
請注意,“無論如何”意味着無狀態函數必須是線程安全的。
如果函數返回void
,它是否總是無狀態? 嗯......還有另一個stateless
的含義 - 調用無狀態函數不應該有對應用程序“重要”的副作用。
如果func
沒有“重要”副作用,則可以安全地任意調用func
。 例如,即使在同一個元素上, stream.map(func)
也可以安全地多次調用func
。 (但不要擔心, Stream
永遠不會這樣做)。
什么是“重要”副作用? 這是非常主觀的。
至少,調用fun
會花費一些CPU時間,這不是完全免費的。 這可能與性能關鍵應用有關; 或者在昂貴的平台上(咳嗽AWS)。
如果func
在硬盤上記錄某些東西,它可能是也可能不是“重要的”副作用。 (它太貴了)
如果func
查詢成本高昂的外部服務,那么它非常令人擔憂,它可能會使您破產。
現在,忘了錢。 純粹從應用程序邏輯的角度來看, func
可能會導致應用程序所依賴的某些狀態的突變; 即使func
為相同的輸入返回相同的輸出,它仍然不能被視為“無狀態”。 例如,如果在stream.map(func)
, func
將每個元素添加到列表中,稍后應用程序使用該列表,結果列表將取決於在運行時調用func
方式。 這是功能程序員所不知道的。
如果我們做stream.forEach( e->log(e) )
,它是無國籍的嗎? 我們可以認為它是無國籍的
log
的成本 log()
可以同時調用
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.