Sao chép luồng để tránh "luồng đã được vận hành hoặc đã đóng"


121

Tôi muốn sao chép một luồng Java 8 để tôi có thể xử lý nó hai lần. Tôi có thể collectlàm danh sách và nhận các luồng mới từ đó;

// doSomething() returns a stream
List<A> thing = doSomething().collect(toList());
thing.stream()... // do stuff
thing.stream()... // do other stuff

Nhưng tôi nghĩ rằng nên có một cách hiệu quả / thanh lịch hơn.

Có cách nào để sao chép luồng mà không chuyển luồng đó thành một bộ sưu tập không?

Tôi thực sự đang làm việc với một luồng Eithers, vì vậy muốn xử lý hình chiếu bên trái theo một cách trước khi chuyển sang hình chiếu bên phải và xử lý theo cách khác. Kiểu như thế này (mà cho đến nay, tôi buộc phải sử dụng toListthủ thuật với).

List<Either<Pair<A, Throwable>, A>> results = doSomething().collect(toList());

Stream<Pair<A, Throwable>> failures = results.stream().flatMap(either -> either.left());
failures.forEach(failure -> ... );

Stream<A> successes = results.stream().flatMap(either -> either.right());
successes.forEach(success -> ... );

Bạn có thể nói rõ hơn về "quy trình một chiều" ... bạn có đang tiêu thụ các đối tượng không? Lập bản đồ chúng? partitionBy () và groupingBy () có thể đưa bạn trực tiếp đến hơn 2 danh sách, nhưng bạn có thể hưởng lợi từ việc ánh xạ trước hoặc chỉ có một nhánh quyết định trong forEach () của bạn.
AjahnCharles

Trong một số trường hợp, biến nó thành Bộ sưu tập không thể là một tùy chọn nếu chúng ta đang xử lý luồng vô hạn. Bạn có thể tìm thấy một giải pháp thay thế cho ghi nhớ tại đây: dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

Câu trả lời:


88

Tôi nghĩ rằng giả định của bạn về hiệu quả là loại ngược lại. Bạn sẽ nhận được khoản hoàn vốn hiệu quả khổng lồ này nếu bạn chỉ sử dụng dữ liệu một lần, vì bạn không phải lưu trữ dữ liệu và các luồng cung cấp cho bạn các tính năng tối ưu hóa "kết hợp vòng lặp" mạnh mẽ cho phép bạn chuyển toàn bộ dữ liệu một cách hiệu quả qua đường ống.

Nếu bạn muốn sử dụng lại cùng một dữ liệu, thì theo định nghĩa, bạn phải tạo nó hai lần (một cách xác định) hoặc lưu trữ nó. Nếu nó đã có trong một bộ sưu tập, thật tuyệt; sau đó lặp lại nó hai lần là rẻ.

Chúng tôi đã thử nghiệm trong thiết kế với "luồng phân nhánh". Những gì chúng tôi nhận thấy là hỗ trợ điều này có chi phí thực sự; nó tạo gánh nặng cho trường hợp phổ biến (sử dụng một lần) với chi phí của trường hợp không phổ biến. Vấn đề lớn là giải quyết "điều gì sẽ xảy ra khi hai đường ống không tiêu thụ dữ liệu ở cùng một tốc độ." Bây giờ bạn vẫn quay lại bộ đệm. Đây là một tính năng rõ ràng không có trọng lượng của nó.

Nếu bạn muốn thao tác lặp lại trên cùng một dữ liệu, hãy lưu trữ dữ liệu đó hoặc cấu trúc hoạt động của bạn với tư cách Người tiêu dùng và thực hiện như sau:

stream()...stuff....forEach(e -> { consumerA(e); consumerB(e); });

Bạn cũng có thể xem xét thư viện RxJava, vì mô hình xử lý của nó cho phép bản thân nó tốt hơn đối với loại "stream forking".


1
Có lẽ tôi không nên sử dụng "hiệu quả", tôi đang hiểu tại sao tôi lại bận tâm với các luồng (và không lưu trữ bất cứ thứ gì) nếu tất cả những gì tôi làm là lưu trữ ngay lập tức dữ liệu ( toList) để có thể xử lý nó ( Eithertrường hợp là ví dụ)?
Toby

11
Luồng vừa mang tính biểu cảm vừa hiệu quả . Chúng thể hiện ở chỗ chúng cho phép bạn thiết lập các hoạt động tổng hợp phức tạp mà không có nhiều chi tiết ngẫu nhiên (ví dụ: kết quả trung gian) theo cách đọc mã. Chúng cũng hiệu quả, ở chỗ chúng (nói chung) thực hiện một lần chuyển dữ liệu và không điền các vùng chứa kết quả trung gian. Hai thuộc tính này kết hợp với nhau làm cho chúng trở thành một mô hình lập trình hấp dẫn cho nhiều tình huống. Tất nhiên, không phải tất cả các mô hình lập trình đều phù hợp với mọi vấn đề; bạn vẫn cần quyết định xem bạn có đang sử dụng một công cụ thích hợp cho công việc hay không.
Brian Goetz

1
Nhưng việc không thể sử dụng lại luồng gây ra các tình huống trong đó nhà phát triển buộc phải lưu trữ các kết quả trung gian (thu thập) để xử lý luồng theo hai cách khác nhau. Hàm ý rằng luồng được tạo nhiều lần (trừ khi bạn thu thập) có vẻ rõ ràng - bởi vì nếu không, bạn sẽ không cần phương thức thu thập.
Niall Connaughton

@NiallConnaughton Tôi không chắc muốn quan điểm của bạn là. Nếu bạn muốn duyệt nó hai lần, ai đó phải lưu trữ nó, hoặc bạn phải tạo lại nó. Bạn có đề nghị thư viện nên đệm nó trong trường hợp ai đó cần nó hai lần không? Điều đó thật ngớ ngẩn.
Brian Goetz

Không gợi ý rằng thư viện nên đệm nó, nhưng nói rằng bằng cách có các luồng là một lần duy nhất, nó buộc những người muốn sử dụng lại luồng hạt giống (tức là: chia sẻ logic khai báo được sử dụng để định nghĩa nó) để xây dựng nhiều luồng dẫn xuất để thu thập luồng hạt giống hoặc có quyền truy cập vào nhà máy cung cấp sẽ tạo bản sao của luồng hạt giống. Cả hai lựa chọn đều có điểm đau của chúng. Câu trả lời này có nhiều chi tiết hơn về chủ đề: stackoverflow.com/a/28513908/114200 .
Niall Connaughton

73

Bạn có thể sử dụng một biến cục bộ với một Supplierđể thiết lập các phần chung của đường dẫn luồng.

Từ http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ :

Sử dụng lại các luồng

Java 8 luồng không thể được sử dụng lại. Ngay sau khi bạn gọi bất kỳ thao tác đầu cuối nào, luồng sẽ bị đóng:

Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c")
    .filter(s -> s.startsWith("a"));
stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception

Calling `noneMatch` after `anyMatch` on the same stream results in the following exception:
java.lang.IllegalStateException: stream has already been operated upon or closed
at 
java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at 
java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
at com.winterbe.java8.Streams5.test7(Streams5.java:38)
at com.winterbe.java8.Streams5.main(Streams5.java:28)

Để khắc phục hạn chế này, chúng ta phải tạo một chuỗi luồng mới cho mọi thao tác đầu cuối mà chúng ta muốn thực hiện, ví dụ: chúng ta có thể tạo một nhà cung cấp luồng để xây dựng một luồng mới với tất cả các thao tác trung gian đã được thiết lập:

Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
            .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Mỗi cuộc gọi để get()xây dựng một luồng mới mà chúng ta sẽ lưu để gọi hoạt động đầu cuối mong muốn.


2
giải pháp tốt đẹp và thanh lịch. nhiều hơn java8-ish so với giải pháp được ủng hộ nhiều nhất.
dylaniato

Chỉ cần một ghi chú về cách sử dụng Suppliernếu Streamđược xây dựng với một cách "tốn kém", bạn phải trả chi phí đó cho mỗi cuộc gọi đếnSupplier.get() . tức là nếu một truy vấn cơ sở dữ liệu ... truy vấn đó được thực hiện mỗi lần
Julien

Bạn dường như không thể làm theo mẫu này sau mapTo mặc dù sử dụng IntStream. Tôi thấy rằng tôi phải chuyển đổi nó trở lại Set<Integer>sử dụng collect(Collectors.toSet())... và thực hiện một vài thao tác trên đó. Tôi muốn max()và nếu một giá trị cụ thể được đặt thành hai phép toán ...filter(d -> d == -1).count() == 1;
JGFMK

16

Sử dụng a Supplierđể tạo luồng cho mỗi hoạt động kết thúc.

Supplier<Stream<Integer>> streamSupplier = () -> list.stream();

Bất cứ khi nào bạn cần một luồng của bộ sưu tập đó, hãy sử dụng streamSupplier.get()để nhận một luồng mới.

Ví dụ:

  1. streamSupplier.get().anyMatch(predicate);
  2. streamSupplier.get().allMatch(predicate2);

Ủng hộ bạn vì bạn là người đầu tiên chỉ ra các Nhà cung cấp ở đây.
EnzoBnl

9

Chúng tôi đã triển khai một duplicate()phương pháp cho các luồng trong jOOλ , một thư viện Nguồn mở mà chúng tôi đã tạo để cải thiện kiểm tra tích hợp cho jOOQ . Về cơ bản, bạn chỉ có thể viết:

Tuple2<Seq<A>, Seq<A>> duplicates = Seq.seq(doSomething()).duplicate();

Bên trong, có một bộ đệm lưu trữ tất cả các giá trị đã được sử dụng từ một luồng nhưng không từ luồng khác. Điều đó có thể hiệu quả như nó đạt được nếu hai luồng của bạn được sử dụng ở cùng một tốc độ và nếu bạn có thể sống với việc thiếu an toàn luồng .

Đây là cách thuật toán hoạt động:

static <T> Tuple2<Seq<T>, Seq<T>> duplicate(Stream<T> stream) {
    final List<T> gap = new LinkedList<>();
    final Iterator<T> it = stream.iterator();

    @SuppressWarnings("unchecked")
    final Iterator<T>[] ahead = new Iterator[] { null };

    class Duplicate implements Iterator<T> {
        @Override
        public boolean hasNext() {
            if (ahead[0] == null || ahead[0] == this)
                return it.hasNext();

            return !gap.isEmpty();
        }

        @Override
        public T next() {
            if (ahead[0] == null)
                ahead[0] = this;

            if (ahead[0] == this) {
                T value = it.next();
                gap.offer(value);
                return value;
            }

            return gap.poll();
        }
    }

    return tuple(seq(new Duplicate()), seq(new Duplicate()));
}

Thêm mã nguồn tại đây

Tuple2có lẽ như bạn Pairchủng loại, trong khi đó SeqStreamvới một số cải tiến.


2
Giải pháp này không an toàn cho luồng: bạn không thể chuyển một trong các luồng sang luồng khác. Tôi thực sự không thấy kịch bản nào khi cả hai luồng có thể được tiêu thụ với tỷ lệ ngang nhau trong một luồng đơn lẻ và bạn thực sự cần hai luồng riêng biệt. Nếu bạn muốn tạo ra hai kết quả từ cùng một luồng, sẽ tốt hơn nhiều nếu sử dụng kết hợp các bộ thu (mà bạn đã có trong JOOL).
Tagir Valeev

@TagirValeev: Bạn nói đúng về sự an toàn của luồng, điểm tốt. Làm thế nào điều này có thể được thực hiện với việc kết hợp các nhà sưu tập?
Lukas Eder

1
Ý tôi là nếu ai đó muốn sử dụng cùng một luồng hai lần như thế này Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, thì tốt hơn hết Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. Việc sử dụng Collectors.mapping/reducingmột có thể thể hiện các hoạt động luồng khác như bộ thu thập và xử lý các phần tử theo cách khá khác nhau để tạo ra một bộ tuple kết quả duy nhất. Vì vậy, nói chung, bạn có thể làm nhiều việc sử dụng luồng một lần mà không bị trùng lặp và nó sẽ thân thiện với song song.
Tagir Valeev

2
Trong trường hợp này, bạn sẽ vẫn giảm hết luồng này đến luồng khác. Vì vậy, không có lý do gì để làm cho cuộc sống khó khăn hơn khi giới thiệu trình lặp được làm mềm hóa mà dù sao cũng sẽ thu thập toàn bộ luồng vào danh sách ẩn. Bạn có thể chỉ cần thu thập vào danh sách một cách rõ ràng sau đó tạo hai luồng từ đó như OP yêu cầu (đó là cùng một số dòng mã). Chà, bạn có thể chỉ cải thiện được một số nếu lần giảm đầu tiên là ngắn mạch, nhưng nó không phải là trường hợp OP.
Tagir Valeev

1
@maaartinus: Cảm ơn, bạn tốt. Tôi đã tạo ra một vấn đề cho điểm chuẩn. Tôi đã sử dụng nó cho offer()/ poll()API, nhưng một cũng ArrayDequecó thể làm như vậy.
Lukas Eder

7

Bạn có thể tạo một dòng các khả năng chạy (ví dụ):

results.stream()
    .flatMap(either -> Stream.<Runnable> of(
            () -> failure(either.left()),
            () -> success(either.right())))
    .forEach(Runnable::run);

Các thao tác cần áp dụng ở đâu failurevà ở đâu success. Tuy nhiên, điều này sẽ tạo ra một số đối tượng tạm thời và có thể không hiệu quả hơn việc bắt đầu từ một bộ sưu tập và phát trực tuyến / lặp lại nó hai lần.


4

Một cách khác để xử lý các phần tử nhiều lần là sử dụng Stream.peek (Người tiêu dùng) :

doSomething().stream()
.peek(either -> handleFailure(either.left()))
.foreach(either -> handleSuccess(either.right()));

peek(Consumer) có thể được xâu chuỗi nhiều lần nếu cần.

doSomething().stream()
.peek(element -> handleFoo(element.foo()))
.peek(element -> handleBar(element.bar()))
.peek(element -> handleBaz(element.baz()))
.foreach(element-> handleQux(element.qux()));

Có vẻ như cái nhìn không phải được sử dụng cho điều này (xem softwareengineering.stackexchange.com/a/308979/195787 )
HectorJ

2
@HectorJ Chủ đề khác là về sửa đổi các phần tử. Tôi cho rằng điều đó không được thực hiện ở đây.
Martin

2

cyclops-react , một thư viện mà tôi đóng góp, có một phương thức tĩnh cho phép bạn sao chép một Luồng (và trả về một jOOλ Tuple of Streams).

    Stream<Integer> stream = Stream.of(1,2,3);
    Tuple2<Stream<Integer>,Stream<Integer>> streams =  StreamUtils.duplicate(stream);

Xem nhận xét, sẽ có hình phạt về hiệu suất sẽ phát sinh khi sử dụng bản sao trên Luồng hiện có. Một giải pháp thay thế hiệu quả hơn sẽ là sử dụng Streamable: -

Ngoài ra còn có một lớp Streamable (lười biếng) có thể được xây dựng từ Stream, Iterable hoặc Array và được phát lại nhiều lần.

    Streamable<Integer> streamable = Streamable.of(1,2,3);
    streamable.stream().forEach(System.out::println);
    streamable.stream().forEach(System.out::println);

AsStreamable.synchronizedFromStream (luồng) - có thể được sử dụng để tạo một Streamable sẽ đưa vào bộ sưu tập sao lưu của nó một cách lười biếng, theo cách có thể được chia sẻ trên các luồng. Streamable.fromStream (luồng) sẽ không phải chịu bất kỳ chi phí đồng bộ hóa nào.


2
Và, tất nhiên cần lưu ý rằng các luồng kết quả có tổng chi phí CPU / bộ nhớ đáng kể và hiệu suất song song rất kém. Ngoài ra, giải pháp này không an toàn cho luồng (bạn không thể chuyển một trong các luồng kết quả sang luồng khác và xử lý song song một cách an toàn). Nó sẽ hiệu quả và an toàn hơn nhiều List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(như OP đề xuất). Ngoài ra, vui lòng tiết lộ rõ ​​ràng trong câu trả lời rằng bạn là tác giả của các luồng cyclop. Đọc cái này .
Tagir Valeev

Cập nhật để phản ánh tôi là tác giả. Cũng là một điểm tốt để thảo luận về đặc điểm hiệu suất của từng loại. Đánh giá của bạn ở trên là khá nhiều điểm cho StreamUtils.duplicate. StreamUtils.duplicate hoạt động bằng cách đệm dữ liệu từ Luồng này sang Luồng khác, phát sinh chi phí cả CPU và Bộ nhớ (tùy theo trường hợp sử dụng). Tuy nhiên, đối với Streamable.of (1,2,3), một Luồng mới được tạo trực tiếp từ Mảng mỗi lần và các đặc tính hiệu suất, bao gồm cả hiệu suất song song, sẽ giống như đối với Luồng được tạo thông thường.
John McClean

Ngoài ra, có một lớp AsStreamable cho phép tạo một cá thể Streamable từ một Stream nhưng đồng bộ hóa quyền truy cập vào bộ sưu tập hỗ trợ Streamable khi nó được tạo (AsStreamable.synchronizedFromStream). Làm cho nó phù hợp hơn để sử dụng trên các luồng (nếu đó là những gì bạn cần - tôi sẽ tưởng tượng 99% thời gian Luồng được tạo và sử dụng lại trên cùng một luồng).
John McClean

Xin chào Tagir - bạn cũng không nên tiết lộ trong nhận xét của mình rằng bạn là tác giả của một thư viện cạnh tranh?
John McClean

1
Nhận xét không phải là câu trả lời và tôi không quảng cáo thư viện của mình ở đây vì thư viện của tôi không có tính năng sao chép luồng (chỉ vì tôi nghĩ nó vô dụng), vì vậy chúng tôi không cạnh tranh ở đây. Tất nhiên khi tôi đề xuất một giải pháp liên quan đến thư viện của mình, tôi luôn nói rõ ràng rằng tôi là tác giả.
Tagir Valeev

0

Đối với vấn đề cụ thể này, bạn cũng có thể sử dụng phân vùng. Cái gì đó như

     // Partition Eighters into left and right
     List<Either<Pair<A, Throwable>, A>> results = doSomething();
     Map<Boolean, Object> passingFailing = results.collect(Collectors.partitioningBy(s -> s.isLeft()));
     passingFailing.get(true) <- here will be all passing (left values)
     passingFailing.get(false) <- here will be all failing (right values)

0

Chúng tôi có thể sử dụng Trình tạo luồng tại thời điểm đọc hoặc lặp lại luồng. Đây là tài liệu của Stream Builder .

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.Builder.html

Ca sử dụng

Giả sử chúng ta có luồng nhân viên và chúng ta cần sử dụng luồng này để ghi dữ liệu nhân viên trong tệp excel và sau đó cập nhật bảng / bộ sưu tập nhân viên [Đây chỉ là trường hợp sử dụng để hiển thị việc sử dụng Trình tạo luồng]:

Stream.Builder<Employee> builder = Stream.builder();

employee.forEach( emp -> {
   //store employee data to excel file 
   // and use the same object to build the stream.
   builder.add(emp);
});

//Now this stream can be used to update the employee collection
Stream<Employee> newStream = builder.build();

0

Tôi gặp vấn đề tương tự và có thể nghĩ ra ba cấu trúc trung gian khác nhau để tạo bản sao của luồng: a List, mảng và a Stream.Builder. Tôi đã viết một chương trình điểm chuẩn nhỏ, đề xuất rằng từ quan điểm hiệu suất,List chậm hơn khoảng 30% so với hai chương trình khác khá giống nhau.

Hạn chế duy nhất của việc chuyển đổi thành một mảng là rất khó nếu kiểu phần tử của bạn là kiểu chung (trong trường hợp của tôi là như vậy); do đó tôi thích sử dụngStream.Builder .

Tôi đã kết thúc việc viết một hàm nhỏ tạo ra Collector:

private static <T> Collector<T, Stream.Builder<T>, Stream<T>> copyCollector()
{
    return Collector.of(Stream::builder, Stream.Builder::add, (b1, b2) -> {
        b2.build().forEach(b1);
        return b1;
    }, Stream.Builder::build);
}

Sau đó, tôi có thể tạo một bản sao của bất kỳ luồng nào strbằng cách làm str.collect(copyCollector())mà cảm thấy khá phù hợp với cách sử dụng thành ngữ của luồng.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.