簡體   English   中英

Java 8 Streams:復雜的流處理

[英]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.htmlhttp:// docs中很好地描述了中間和終端無狀態及有狀態操作的所有復雜性.oracle.com / javase / 8 / docs / api / java / util / stream / Stream.html

這種方法既有優點也有缺點。 一個顯着的優點是它允許並行處理流。 一個重大的缺點是,在某些其他語言中容易執行的操作(例如,跳過流中的每個第三個元素)在Java中很難實現。

請注意,您將看到很多代碼(包括SO上公認的答案),忽略了有關流操作的行為參數應為無狀態的建議。 為了工作,此代碼依賴於語言規范未定義的Java實現的行為:即按順序處理流。 規范中沒有什么可以阻止Java處理元素以相反順序或隨機順序實現。 這樣的實現將使任何有狀態的流操作立即表現出不同的行為。 無狀態操作將繼續表現完全相同。 因此,總而言之,有狀態操作依賴於Java 實現的細節而不是規范

還請注意,可以進行安全的有狀態中間操作。 需要對它們進行設計,以使它們明確不依賴於處理元素的順序。 Stream.distinctStream.sorted就是很好的例子。 它們需要保持工作狀態,但是設計目的是不管處理元素的順序如何。

因此,要回答您的問題,可以在Java中完成這些類型的操作,但是它們不簡單,安全(出於上一段給出的原因),也不自然適合於語言設計。 我建議使用縮減或收集或(請參見Tagir Valeev的答案)分離器創建新的流。 或者使用傳統迭代。

您可以調用並返回任何標准流操作,例如filtermapreduce等,然后讓它們執行一些復雜的操作,例如需要外部數據的操作。 例如, filterAdjacentDuplicatesreplaceNthElement可以實現這樣的:

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 常用算法如下:

  1. 使用stream.spliterator stream.spliterator()獲取現有的Stream Spliterator
  2. 編寫自己的Spliterator,在進行一些其他操作時可能會消耗現有分離器的元素。
  3. 通過StreamSupport.stream(spliterator, stream.isParallel())基於您的分隔器創建一個新流
  4. 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM