Salin aliran untuk menghindari "aliran telah dioperasikan atau ditutup"


121

Saya ingin menduplikasi aliran Java 8 sehingga saya bisa mengatasinya dua kali. Saya bisa collectsebagai daftar dan mendapatkan aliran baru dari itu;

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

Tapi menurut saya harus ada cara yang lebih efisien / elegan.

Adakah cara untuk menyalin aliran tanpa mengubahnya menjadi koleksi?

Saya sebenarnya bekerja dengan aliran Eithers, jadi ingin memproses proyeksi kiri dengan satu cara sebelum beralih ke proyeksi kanan dan menangani dengan cara lain. Jenis seperti ini (yang, sejauh ini, saya terpaksa menggunakan toListtrik dengan).

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 -> ... );

Bisakah Anda menjelaskan lebih lanjut tentang "proses satu cara" ... apakah Anda mengonsumsi objek? Memetakan mereka? partitionBy () dan groupingBy () dapat membawa Anda langsung ke 2+ daftar, tetapi Anda mungkin mendapatkan keuntungan dari pemetaan terlebih dahulu atau hanya memiliki garpu keputusan di forEach () Anda.
AjahnCharles

Dalam beberapa kasus, mengubahnya menjadi Koleksi tidak bisa menjadi pilihan jika kita berurusan dengan aliran tak terbatas. Anda dapat menemukan alternatif untuk memoisasi di sini: dzone.com/articles/how-to-replay-java-streams
Miguel Gamboa

Jawaban:


88

Saya pikir asumsi Anda tentang efisiensi agak mundur. Anda akan mendapatkan pengembalian efisiensi yang sangat besar ini jika Anda hanya akan menggunakan data sekali, karena Anda tidak perlu menyimpannya, dan streaming memberi Anda pengoptimalan "loop fusion" yang hebat yang memungkinkan Anda mengalirkan seluruh data secara efisien melalui pipeline.

Jika Anda ingin menggunakan kembali data yang sama, maka menurut definisi Anda harus menghasilkannya dua kali (secara deterministik) atau menyimpannya. Jika kebetulan sudah ada dalam koleksi, bagus; kemudian mengulanginya dua kali lebih murah.

Kami melakukan percobaan dalam desain dengan "aliran bercabang". Apa yang kami temukan adalah bahwa mendukung ini memiliki biaya yang nyata; itu membebani kasus umum (gunakan sekali) dengan mengorbankan kasus yang tidak umum. Masalah besarnya adalah berurusan dengan "apa yang terjadi jika kedua pipeline tidak mengonsumsi data pada kecepatan yang sama". Sekarang Anda kembali ke buffering. Ini adalah fitur yang jelas tidak membawa bobotnya.

Jika Anda ingin mengoperasikan data yang sama berulang kali, simpan, atau susun operasi Anda sebagai Konsumen dan lakukan hal berikut:

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

Anda juga dapat melihat ke perpustakaan RxJava, karena model pemrosesannya lebih cocok untuk jenis "percabangan aliran" ini.


1
Mungkin saya seharusnya tidak menggunakan "efisiensi", saya agak mengerti mengapa saya harus repot-repot dengan aliran (dan tidak menyimpan apa pun) jika yang saya lakukan hanyalah segera menyimpan data ( toList) untuk dapat memprosesnya ( Eitherkasus menjadi contoh)?
Toby

11
Aliran bersifat ekspresif dan efisien . Mereka ekspresif karena memungkinkan Anda menyiapkan operasi agregat kompleks tanpa banyak detail yang tidak disengaja (misalnya, hasil antara) dalam cara membaca kode. Mereka juga efisien, karena (umumnya) membuat satu pass pada data dan tidak mengisi container hasil antara. Kedua properti ini bersama-sama menjadikannya model pemrograman yang menarik untuk banyak situasi. Tentu saja, tidak semua model pemrograman cocok untuk semua masalah; Anda masih perlu memutuskan apakah Anda menggunakan alat yang sesuai untuk pekerjaan itu.
Brian Goetz

1
Tetapi ketidakmampuan untuk menggunakan kembali aliran menyebabkan situasi di mana pengembang dipaksa untuk menyimpan hasil antara (pengumpulan) untuk memproses aliran dalam dua cara berbeda. Implikasi bahwa aliran dibuat lebih dari sekali (kecuali Anda mengumpulkannya) tampak jelas - karena jika tidak, Anda tidak memerlukan metode kumpulkan.
Niall Connaughton

@NiallConnaughton Saya tidak yakin ingin maksud Anda adalah. Jika Anda ingin melintasinya dua kali, seseorang harus menyimpannya, atau Anda harus membuat ulang. Apakah Anda menyarankan agar perpustakaan harus menyanggahnya kalau-kalau ada yang membutuhkannya dua kali? Itu konyol.
Brian Goetz

Tidak menyarankan bahwa perpustakaan harus menyangganya, tetapi mengatakan bahwa dengan memiliki aliran sebagai satu kali, itu memaksa orang yang ingin menggunakan kembali aliran benih (yaitu: berbagi logika deklaratif yang digunakan untuk mendefinisikannya) untuk membangun beberapa aliran turunan untuk mengumpulkan aliran benih, atau memiliki akses ke pabrik penyedia yang akan membuat duplikat aliran benih. Kedua opsi tersebut memiliki titik sakitnya masing-masing. Jawaban ini memiliki lebih banyak detail tentang topik: stackoverflow.com/a/28513908/114200 .
Niall Connaughton

73

Anda dapat menggunakan variabel lokal dengan Supplieruntuk menyiapkan bagian umum dari pipeline streaming.

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

Menggunakan Kembali Arus

Aliran Java 8 tidak dapat digunakan kembali. Segera setelah Anda memanggil operasi terminal apa pun, aliran ditutup:

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)

Untuk mengatasi batasan ini kita harus membuat rantai aliran baru untuk setiap operasi terminal yang ingin kita jalankan, misalnya kita bisa membuat pemasok aliran untuk membangun aliran baru dengan semua operasi perantara sudah diatur:

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

Setiap panggilan ke get() membangun aliran baru yang kita simpan untuk memanggil operasi terminal yang diinginkan.


2
solusi yang bagus dan elegan. lebih banyak java8-ish daripada solusi yang paling banyak dipilih.
dylaniato

Sekadar catatan tentang penggunaan Supplierjika Streamdibangun dengan cara yang "mahal", Anda membayar biaya tersebut untuk setiap panggilan keSupplier.get() . yaitu jika kueri database ... kueri itu dilakukan setiap waktu
Julien

Anda sepertinya tidak bisa mengikuti pola ini setelah mapTo meskipun menggunakan IntStream. Saya menemukan saya harus mengubahnya kembali menjadi Set<Integer>penggunaan collect(Collectors.toSet())... dan melakukan beberapa operasi untuk itu. Saya ingin max()dan jika nilai tertentu ditetapkan sebagai dua operasi ...filter(d -> d == -1).count() == 1;
JGFMK

16

Gunakan a Supplieruntuk menghasilkan aliran untuk setiap operasi terminasi.

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

Kapan pun Anda membutuhkan aliran koleksi itu, gunakan streamSupplier.get()untuk mendapatkan aliran baru.

Contoh:

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

Berikan suara positif kepada Anda karena Anda adalah orang pertama yang menunjuk Pemasok di sini.
EnzoBnl

9

Kami telah menerapkan duplicate()metode untuk aliran di jOOλ , pustaka Sumber Terbuka yang kami buat untuk meningkatkan pengujian integrasi untuk jOOQ . Intinya, Anda cukup menulis:

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

Secara internal, ada buffer yang menyimpan semua nilai yang telah dikonsumsi dari satu aliran tetapi tidak dari yang lain. Itu mungkin seefisien yang didapat jika dua aliran Anda dikonsumsi dengan kecepatan yang sama, dan jika Anda dapat hidup dengan kurangnya keamanan utas .

Berikut cara kerja algoritme:

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()));
}

Lebih banyak kode sumber di sini

Tuple2mungkin seperti Anda Pairjenis, sedangkan Seqadalah Streamdengan beberapa perangkat tambahan.


2
Solusi ini tidak aman untuk thread: Anda tidak dapat meneruskan salah satu aliran ke thread lain. Saya benar-benar tidak melihat skenario ketika kedua aliran dapat dikonsumsi dalam kecepatan yang sama dalam satu utas dan Anda benar-benar membutuhkan dua aliran berbeda. Jika Anda ingin menghasilkan dua hasil dari aliran yang sama, akan lebih baik jika menggunakan kolektor gabungan (yang sudah Anda miliki di JOOL).
Tagir Valeev

@TagirValeev: Anda benar tentang keamanan thread, poin yang bagus. Bagaimana ini bisa dilakukan dengan menggabungkan kolektor?
Lukas Eder

1
Maksud saya, jika seseorang ingin menggunakan streaming yang sama dua kali seperti ini Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, lebih baik melakukannya Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. Menggunakan Collectors.mapping/reducingsatu dapat mengekspresikan operasi aliran lain sebagai kolektor dan elemen proses dengan cara yang sangat berbeda menciptakan tupel yang dihasilkan tunggal. Jadi secara umum Anda dapat melakukan banyak hal yang memakan aliran satu kali tanpa duplikasi dan itu akan ramah paralel.
Tagir Valeev

2
Dalam hal ini, Anda masih akan mengurangi aliran satu demi satu. Jadi tidak ada gunanya membuat hidup lebih sulit dengan memperkenalkan iterator canggih yang bagaimanapun akan mengumpulkan seluruh aliran ke daftar di bawah tenda. Anda hanya dapat mengumpulkan ke daftar secara eksplisit kemudian membuat dua aliran darinya seperti yang dikatakan OP (itu adalah jumlah baris kode yang sama). Nah, Anda mungkin hanya mengalami beberapa perbaikan jika pengurangan pertama adalah korsleting, tetapi itu bukan kasus OP.
Tagir Valeev

1
@maaartinus: Terima kasih, penunjuk yang bagus. Saya telah membuat masalah untuk benchmark. Saya menggunakannya untuk offer()/ poll()API, tetapi ArrayDequemungkin melakukan hal yang sama.
Lukas Eder

7

Anda dapat membuat aliran runnable (misalnya):

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

Di mana failuredan successoperasi yang akan diterapkan. Namun ini akan membuat beberapa objek sementara dan mungkin tidak lebih efisien daripada memulai dari pengumpulan dan streaming / iterasi dua kali.


4

Cara lain untuk menangani elemen beberapa kali adalah dengan menggunakan Stream.peek (Konsumen) :

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

peek(Consumer) dapat dirantai sebanyak yang dibutuhkan.

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

Tampaknya mengintip tidak seharusnya digunakan untuk ini (lihat softwareengineering.stackexchange.com/a/308979/195787 )
HectorJ

2
@HectorJ Utas lainnya adalah tentang memodifikasi elemen. Saya berasumsi itu tidak dilakukan di sini.
Martin

2

cyclops-react , pustaka tempat saya berkontribusi, memiliki metode statis yang memungkinkan Anda menduplikasi Stream (dan mengembalikan jOOλ Tuple of Streams).

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

Lihat komentar, ada hukuman kinerja yang akan ditimbulkan saat menggunakan duplikat di Aliran yang ada. Alternatif yang lebih berkinerja adalah menggunakan Streamable: -

Ada juga kelas Streamable (malas) yang dapat dibuat dari Stream, Iterable atau Array dan diputar ulang beberapa kali.

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

AsStreamable.synchronizedFromStream (stream) - dapat digunakan untuk membuat Streamable yang dengan malas akan mengisi koleksi pendukungnya, sedemikian rupa sehingga dapat dibagikan di seluruh utas. Streamable.fromStream (streaming) tidak akan menimbulkan overhead sinkronisasi.


2
Dan, tentu saja perlu dicatat bahwa aliran yang dihasilkan memiliki kelebihan CPU / memori yang signifikan dan kinerja paralel yang sangat buruk. Juga solusi ini tidak aman untuk thread (Anda tidak dapat meneruskan salah satu aliran yang dihasilkan ke thread lain dan memprosesnya dengan aman secara paralel). Akan jauh lebih berkinerja dan aman List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(seperti yang disarankan OP). Juga ungkapkan secara eksplisit dalam jawaban bahwa Anda adalah penulis cyclop-streams. Baca ini .
Tagir Valeev

Diperbarui untuk mencerminkan bahwa saya adalah penulisnya. Juga poin yang baik untuk membahas karakteristik kinerja masing-masing. Penilaian Anda di atas cukup tepat untuk StreamUtils.duplicate. StreamUtils.duplicate bekerja dengan melakukan buffering data dari satu Stream ke Stream lainnya, menyebabkan overhead CPU dan Memori (tergantung kasus penggunaan). Namun, untuk Streamable.of (1,2,3), Stream baru dibuat langsung dari Array setiap kali dan karakteristik performa, termasuk performa paralel, akan sama seperti Stream yang biasanya dibuat.
John McClean

Selain itu, ada kelas AsStreamable yang memungkinkan pembuatan instance Streamable dari Stream tetapi menyinkronkan akses ke koleksi yang mendukung Streamable saat dibuat (AsStreamable.synchronizedFromStream). Menjadikannya lebih cocok untuk digunakan di seluruh utas (jika itu yang Anda butuhkan - Saya akan membayangkan 99% dari waktu Stream dibuat dan digunakan kembali pada utas yang sama).
John McClean

Hai Tagir - bukankah seharusnya Anda juga mengungkapkan dalam komentar Anda bahwa Anda adalah penulis perpustakaan yang bersaing?
John McClean

1
Komentar bukanlah jawaban dan saya tidak mengiklankan perpustakaan saya di sini karena perpustakaan saya tidak memiliki fitur untuk menggandakan aliran (hanya karena menurut saya tidak berguna), jadi kami tidak bersaing di sini. Tentu saja ketika saya mengusulkan solusi yang melibatkan perpustakaan saya, saya selalu mengatakan secara eksplisit bahwa saya adalah penulisnya.
Tagir Valeev

0

Untuk masalah khusus ini, Anda juga dapat menggunakan partisi. Sesuatu seperti

     // 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

Kami dapat menggunakan Stream Builder pada saat membaca atau mengulang aliran. Berikut dokumen Stream Builder .

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

Kasus penggunaan

Katakanlah kita memiliki aliran karyawan dan kita perlu menggunakan aliran ini untuk menulis data karyawan dalam file excel dan kemudian memperbarui koleksi / tabel karyawan [Ini hanya kasus penggunaan untuk menunjukkan penggunaan Stream Builder]:

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

Saya memiliki masalah yang sama, dan dapat memikirkan tiga struktur perantara yang berbeda untuk membuat salinan aliran: a List, array, dan a Stream.Builder. Saya menulis program benchmark kecil, yang menyarankan bahwa dari sudut pandang kinerjaList sekitar 30% lebih lambat daripada dua lainnya yang cukup mirip.

Satu-satunya kelemahan dari mengonversi ke array adalah rumit jika tipe elemen Anda adalah tipe generik (yang dalam kasus saya memang demikian); oleh karena itu saya lebih suka menggunakan aStream.Builder .

Saya akhirnya menulis sebuah fungsi kecil yang menciptakan 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);
}

Saya kemudian dapat membuat salinan aliran apa pun strdengan melakukan str.collect(copyCollector())yang dirasa cukup sesuai dengan penggunaan idiomatik aliran.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.