[英]java 8 streams: complex stream processing
我想創建一種對流執行一些復雜操作的方法(例如,替換第7個元素, 刪除最后一個元素, 刪除相鄰的重復項等),而不緩存整個流。
但是什么流API可以讓我插入此方法? 我是否必須創建自己的收集器,以便在收集時將物品發射到其他流? 但這會改變數據流方向,從拉到推,對吧?
這種方法的可能簽名是什么?
Stream<T> process(Stream<T> in)
可能是不可能的(在單線程代碼中),因為只有在收集整個輸入流之后才能返回結果
另一個想法:
void process(Stream<T> in, Stream<T> out)
也似乎有點缺陷,因為java不允許發出將項目插入現有流(作為out
參數提供)的聲明。
那么我該如何在Java中進行一些復雜的流處理呢?
您用作示例的復雜操作全部遵循對流中一個元素的操作模式,具體取決於流中的其他元素。 Java流經過專門設計,不允許在沒有收集或精簡的情況下進行這些類型的操作。 流操作不允許直接訪問其他成員,通常,具有副作用的非終端操作是個壞主意。
請注意Stream
javadoc中的以下內容:
集合和流雖然具有一些表面上的相似性,但它們具有不同的目標。 館藏主要涉及對其元素的有效管理和訪問。 相比之下,流不提供直接訪問或操縱其元素的方法,而與聲明性地描述其源以及將在該源上聚合執行的計算操作有關。
進一步來說:
大多數流操作接受描述用戶指定行為的參數。為了保留正確的行為,這些行為參數:
必須是無干擾的(它們不修改流源); 並且在大多數情況下必須是無狀態的(它們的結果不應依賴於在流管道執行期間可能改變的任何狀態)。
和
如果流操作的行為參數是有狀態的,則流管線結果可能不確定或不正確。 有狀態的lambda(或其他實現適當功能接口的對象)是一種有狀態的lambda,其結果取決於流管道執行期間可能更改的任何狀態
在https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html和http:// docs中很好地描述了中間和終端無狀態及有狀態操作的所有復雜性.oracle.com / javase / 8 / docs / api / java / util / stream / Stream.html
這種方法既有優點也有缺點。 一個顯着的優點是它允許並行處理流。 一個重大的缺點是,在某些其他語言中容易執行的操作(例如,跳過流中的每個第三個元素)在Java中很難實現。
請注意,您將看到很多代碼(包括SO上公認的答案),忽略了有關流操作的行為參數應為無狀態的建議。 為了工作,此代碼依賴於語言規范未定義的Java實現的行為:即按順序處理流。 規范中沒有什么可以阻止Java處理元素以相反順序或隨機順序實現。 這樣的實現將使任何有狀態的流操作立即表現出不同的行為。 無狀態操作將繼續表現完全相同。 因此,總而言之,有狀態操作依賴於Java 實現的細節而不是規范 。
還請注意,可以進行安全的有狀態中間操作。 需要對它們進行設計,以使它們明確不依賴於處理元素的順序。 Stream.distinct
和Stream.sorted
就是很好的例子。 它們需要保持工作狀態,但是設計目的是不管處理元素的順序如何。
因此,要回答您的問題,可以在Java中完成這些類型的操作,但是它們不簡單,安全(出於上一段給出的原因),也不自然適合於語言設計。 我建議使用縮減或收集或(請參見Tagir Valeev的答案)分離器創建新的流。 或者使用傳統迭代。
您可以調用並返回任何標准流操作,例如filter
, map
, reduce
等,然后讓它們執行一些復雜的操作,例如需要外部數據的操作。 例如, filterAdjacentDuplicates
和replaceNthElement
可以實現這樣的:
public static <T> Stream<T> filterAdjacentDupes(Stream<T> stream) {
AtomicReference<T> last = new AtomicReference<>();
return stream.filter(t -> ! t.equals(last.getAndSet(t)));
}
public static <T> Stream<T> replaceNthElement(Stream<T> stream, int n, T repl) {
AtomicInteger count = new AtomicInteger();
return stream.map(t -> count.incrementAndGet() == n ? repl : t);
}
用法示例:
List<String> lst = Arrays.asList("foo", "bar", "bar", "bar", "blub", "foo");
replaceNthElement(filterAdjacentDupes(lst.stream()), 3, "BAR").forEach(System.out::println);
// Output: foo bar BAR foo
但是,正如注釋中指出的那樣,這實際上並不是應該使用Stream API的方式。 特別地,當給定並行流時,諸如這兩種操作將失敗。
正確的方法(盡管不是很容易)是編寫自己的Spliterator
。 常用算法如下:
stream.spliterator()
獲取現有的Stream Spliterator StreamSupport.stream(spliterator, stream.isParallel())
基於您的分隔器創建一個新流 close()
調用委托給原始流,如.onClose(stream::close)
。 編寫良好的並行化良好的分離器通常是一項非常艱巨的任務。 但是,如果您不關心並行化,則可以將AbstractSpliterator
子類AbstractSpliterator
,這更簡單。 這是一個示例,該示例如何編寫新的Stream操作以刪除給定位置的元素:
public static <T> Stream<T> removeAt(Stream<T> src, int idx) {
Spliterator<T> spltr = src.spliterator();
Spliterator<T> res = new AbstractSpliterator<T>(Math.max(0, spltr.estimateSize()-1),
spltr.characteristics()) {
long cnt = 0;
@Override
public boolean tryAdvance(Consumer<? super T> action) {
if(cnt++ == idx && !spltr.tryAdvance(x -> {}))
return false;
return spltr.tryAdvance(action);
}
};
return StreamSupport.stream(res, src.isParallel()).onClose(src::close);
}
這是最少的實現,可以進行改進以顯示更好的性能和並行性。
在我的StreamEx庫中,我嘗試通過headTail
簡化此類自定義流操作的headTail
。 這是使用StreamEx
進行相同操作的StreamEx
:
public static <T> StreamEx<T> removeAt(StreamEx<T> src, int idx) {
// head is the first stream element
// tail is the stream of the rest elements
// want to remove first element? ok, just remove tail
// otherwise call itself with decremented idx and prepend the head element to the result
return src.headTail(
(head, tail) -> idx == 0 ? tail : removeAt(tail, idx-1).prepend(head));
}
您甚至可以使用chain()
方法支持鏈接:
public static <T> Function<StreamEx<T>, StreamEx<T>> removeAt(int idx) {
return s -> removeAt(s, idx);
}
用法示例:
StreamEx.of("Java 8", "Stream", "API", "is", "not", "great")
.chain(removeAt(4)).forEach(System.out::println);
最后要注意的是,即使沒有headTail
也有一些方法可以使用StreamEx解決您的問題。 要刪除特定索引,您可以使用遞增的數字進行壓縮,然后像這樣過濾和刪除索引:
StreamEx.of(stream)
.zipWith(IntStreamEx.ints().boxed())
.removeValues(pos -> pos == idx)
.keys();
要折疊相鄰的重復項,有專用的collapse
方法(甚至可以很好地並行化!):
StreamEx.of(stream).collapse(Object::equals);
在此問題/更新2中表達的tobias_k答案和思想的基礎上,我們可能只返回捕獲其局部變量的適當謂詞和Map函數。 (因此,這些函數是有狀態的,這對於流而言並不理想,但是流API中的distinct()
方法也可能是有狀態的)。
這是修改后的代碼:
public class Foo {
public static void run() {
List<String> lst = Arrays.asList("foo", "bar", "bar", "bar", "blub", "foo");
lst.stream()
.filter(Foo.filterAdjacentDupes())
.map(Foo.replaceNthElement(3, "BAR"))
.forEach(System.out::println);
// Output: foo bar BAR foo
}
public static <T> Predicate<T> filterAdjacentDupes() {
final AtomicReference<T> last = new AtomicReference<>();
return t -> ! t.equals(last.getAndSet(t));
}
public static <T> UnaryOperator<T> replaceNthElement(int n, T repl) {
final AtomicInteger count = new AtomicInteger();
return t -> count.incrementAndGet() == n ? repl : t;
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.