"Akışın zaten çalıştırıldığını veya kapatıldığını" önlemek için bir akışı kopyalayın


121

Bir Java 8 akışını kopyalamak istiyorum, böylece onunla iki kez ilgilenebilirim. collectListe olarak yapabilirim ve bundan yeni akışlar alabilirim;

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

Ama daha verimli / zarif bir yol olması gerektiğini düşünüyorum.

Akışı bir koleksiyona dönüştürmeden kopyalamanın bir yolu var mı?

Aslında bir Eithers akışı ile çalışıyorum , bu yüzden sol izdüşümü sağ projeksiyona geçmeden ve bununla başka bir şekilde ilgilenmeden önce bir şekilde işlemek istiyorum. Bunun gibi (ki şimdiye kadar toListhileyi kullanmak zorunda kaldım ).

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

"Tek yönlü işlem" hakkında daha fazla ayrıntı verebilir misiniz ... nesneleri tüketiyor musunuz? Haritalamak mı? partitionBy () ve groupingBy () sizi doğrudan 2+ listeye götürebilir, ancak önce eşlemeden veya sadece forEach () 'de bir karar çatalına sahip olmaktan faydalanabilirsiniz.
AjahnCharles

Bazı durumlarda, sonsuz akışla uğraşıyorsak, onu Koleksiyona dönüştürmek bir seçenek olamaz. Burada hafızaya alma
Miguel Gamboa

Yanıtlar:


88

Verimlilik hakkındaki varsayımınızın biraz ters olduğunu düşünüyorum. Verileri yalnızca bir kez kullanacaksanız, bu büyük verimlilik geri ödemesini elde edersiniz, çünkü verileri depolamak zorunda değilsiniz ve akışlar, tüm verileri ardışık düzen boyunca verimli bir şekilde aktarmanıza izin veren güçlü "döngü füzyonu" optimizasyonları sunar.

Aynı verileri yeniden kullanmak istiyorsanız, tanım gereği onu iki kez (belirleyici olarak) oluşturmanız veya depolamanız gerekir. Zaten bir koleksiyondaysa, harika; daha sonra iki kez yinelemek ucuzdur.

Tasarımda "çatallı akışlar" ile deney yaptık. Bulduğumuz şey, bunu desteklemenin gerçek maliyetleri olduğuydu; olağandışı durum pahasına ortak durumu (bir kez kullanın) yükledi. Büyük sorun, "iki ardışık düzen aynı hızda veri tüketmediğinde ne olacağı" ile ilgilenmekti. Şimdi yine de arabelleğe geri döndün. Bu, ağırlığını açıkça taşımayan bir özellikti.

Aynı veriler üzerinde tekrar tekrar çalışmak istiyorsanız, bunları saklayın veya işlemlerinizi Tüketiciler olarak yapılandırın ve aşağıdakileri yapın:

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

Ayrıca, işleme modeli bu tür "akış çatallamasına" daha iyi uyum sağladığından, RxJava kitaplığına da bakabilirsiniz.


1
Belki de "verimlilik" i kullanmamalıydım, eğer tek yaptığım verileri toListişleyebilmek için hemen depolamaksa ( Eithervaka ) neden akışlarla uğraşayım (ve hiçbir şeyi saklamayayım) diye anlıyorum. örnek olmak)?
Toby

11
Akışları hem anlamlı ve verimli . Kod okuma biçiminde çok fazla tesadüfi ayrıntı (örneğin, ara sonuçlar) olmaksızın karmaşık toplama işlemleri kurmanıza izin vermeleri açısından anlamlıdırlar. Ayrıca (genellikle) veriler üzerinde tek bir geçiş yaptıkları ve ara sonuç kaplarını doldurmadıkları için etkilidirler. Bu iki özellik birlikte onları birçok durum için çekici bir programlama modeli haline getirir. Tabii ki, tüm programlama modelleri tüm problemlere uymaz; yine de iş için uygun bir araç kullanıp kullanmadığınıza karar vermeniz gerekir.
Brian Goetz

1
Ancak bir akışın yeniden kullanılamaması, geliştiricinin bir akışı iki farklı şekilde işlemek için ara sonuçları depolamaya (toplama) zorlandığı durumlara neden olur. Akışın birden fazla üretildiği iması (siz toplamadığınız sürece) açık görünüyor - çünkü aksi takdirde bir toplama yöntemine ihtiyacınız olmazdı.
Niall Connaughton

@NiallConnaughton Ne demek istediğinden emin değilim. Eğer onu iki kez geçmek istiyorsanız, birinin onu saklaması ya da yeniden oluşturmanız gerekir. Birisinin iki kez ihtiyaç duyması durumunda kütüphanenin arabelleğe almasını mı öneriyorsunuz? Bu aptalca olur.
Brian Goetz

Kütüphanenin onu arabelleğe alması gerektiğini öne sürmemek, ancak akışları tek seferlik olarak alarak, bir çekirdek akışını yeniden kullanmak isteyen insanları (yani: onu tanımlamak için kullanılan bildirim mantığını paylaşmak) birden fazla türetilmiş akış oluşturmaya zorladığını söylemek. çekirdek akışı veya çekirdek akışının bir kopyasını oluşturacak bir sağlayıcı fabrikasına erişim. Her iki seçeneğin de acı noktaları vardır. Bu yanıt, konu hakkında çok daha fazla ayrıntı içeriyor: stackoverflow.com/a/28513908/114200 .
Niall Connaughton

73

SupplierAkış boru hattının ortak parçalarını kurmak için a ile yerel bir değişken kullanabilirsiniz .

Gönderen http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ :

Akışları Yeniden Kullanma

Java 8 akışları yeniden kullanılamaz. Herhangi bir terminal işlemini aradığınız anda akış kapatılır:

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)

Bu sınırlamanın üstesinden gelmek için, yürütmek istediğimiz her terminal işlemi için yeni bir akış zinciri oluşturmalıyız, örneğin, önceden ayarlanmış tüm ara işlemlerle yeni bir akış oluşturmak için bir akış tedarikçisi oluşturabiliriz:

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

Her çağrı get(), istenen terminal işlemini çağırmak için kaydettiğimiz yeni bir akış oluşturur.


2
güzel ve zarif bir çözüm. en çok oy alan çözümden çok daha fazla java8-ish.
dylaniato

Kullanımla ilgili bir not, Suppliereğer Stream"maliyetli" bir şekilde oluşturulmuşsa, her arama için bu ücreti ödersinizSupplier.get() . Örneğin bir veritabanı sorgusu ... bu sorgu her seferinde yapılır
Julien

Bir IntStream kullanıyor olsa da bir mapTo'dan sonra bu kalıbı izleyemiyorsunuz. Ben geri dönüştürmek zorunda buldum Set<Integer>kullanarak collect(Collectors.toSet())... ve bu işlemlerle ilgili birkaç yapmak. İstediğim max()ve belirli bir değer iki operasyonlar ... olarak sette isefilter(d -> d == -1).count() == 1;
JGFMK

16

SupplierHer sonlandırma işlemi için akışı üretmek üzere a kullanın .

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

Bu koleksiyonun bir akışına ihtiyacınız olduğunda, streamSupplier.get()yeni bir akış almak için kullanın .

Örnekler:

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

Burada Tedarikçileri ilk gösteren siz olduğunuz için size oy verin.
EnzoBnl

9

Biz uyguladık duplicate()içinde akışları için yöntem jOOλ , biz entegrasyon testleri geliştirmek için oluşturulan bu açık kaynak kodlu kütüphanesinde jOOQ . Esasen şunu yazabilirsiniz:

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

Dahili olarak, bir akıştan tüketilen ancak diğerinden tüketilmeyen tüm değerleri depolayan bir arabellek vardır. İki akışınız aynı oranda tüketilirse ve iş parçacığı güvenliğinden yoksun yaşayabilirseniz, muhtemelen bu kadar etkilidir .

Algoritma şu şekilde çalışır:

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

Daha fazla kaynak kodu burada

Tuple2muhtemelen gibidir Pairoysa türü Seqolan Streambazı geliştirmeler.


2
Bu çözüm iş parçacığı açısından güvenli değildir: akışlardan birini başka bir iş parçacığına geçiremezsiniz. Her iki akışın da tek iş parçacığında eşit oranda tüketilebildiği ve aslında iki farklı akışa ihtiyacınız olduğunda hiçbir senaryo görmüyorum. Aynı akıştan iki sonuç üretmek istiyorsanız, birleştirici toplayıcıları kullanmak çok daha iyi olacaktır (JOOL'da zaten var olan).
Tagir Valeev

@TagirValeev: İplik güvenliği konusunda haklısın, iyi bir nokta. Koleksiyonerleri birleştirerek bu nasıl yapılabilir?
Lukas Eder

1
Demek istediğim, biri aynı akışı böyle iki kez kullanmak isterse Tuple2<Seq<A>>, Seq<A>> t = duplicate(stream); long count = t.collect(counting()); List<A> list = t.collect(toList());, daha iyi Tuple2<Long, List<A>> t = stream.collect(Tuple.collectors(counting(), toList()));. Kullanarak Collectors.mapping/reducingtek elde edilen başlığın oluşturma oldukça farklı bir biçimde kolektörleri ve işlem elemanları gibi diğer akış işlemleri ifade edebilir. Dolayısıyla, genel olarak, akışı çoğaltma yapmadan bir kez tüketen birçok şeyi yapabilirsiniz ve paralel uyumlu olacaktır.
Tagir Valeev

2
Bu durumda, bir akışı birbiri ardına azaltmaya devam edeceksiniz. Dolayısıyla, tüm akışı kaputun altındaki listeye toplayacak olan sofistike yineleyiciyi tanıtmanın hayatı zorlaştırmanın bir anlamı yok. Listeye açıkça toplayabilir ve ardından OP'nin söylediği gibi listeden iki akış oluşturabilirsiniz (aynı sayıda kod satırı). Eh, sadece ilk azaltma kısa devre ise bir miktar iyileşme elde edebilirsiniz, ancak bu OP durumu değildir.
Tagir Valeev

1
@maaartinus: Teşekkürler, iyi işaretçi. Bir için sorun . offer()/ poll()API için kullandım , ancak ArrayDequeaynı şeyi yapabilir.
Lukas Eder

7

Bir runnables akışı oluşturabilirsiniz (örneğin):

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

Uygulanacak işlemler nerede failureve nerede success? Ancak bu, epeyce geçici nesne yaratacaktır ve bir koleksiyondan başlayıp onu iki kez yayınlamaktan / yinelemekten daha verimli olmayabilir.


4

Öğeleri birden çok kez ele almanın başka bir yolu da Stream.peek (Tüketici) kullanmaktır :

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

peek(Consumer) gerektiği kadar zincirlenebilir.

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

Görünüşe göre
peek'in

2
@HectorJ Diğer iş parçacığı öğeleri değiştirmekle ilgili. Bunun burada yapılmadığını varsaydım.
Martin

2

Katkıda bulunduğum bir kütüphane olan cyclops-react , bir Akışı kopyalamanıza izin verecek (ve bir jOOλ Akım Tuple'ı döndüren) statik bir metoda sahip.

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

Yorumlara bakın, mevcut bir Akışta kopyayı kullanırken ortaya çıkacak performans cezası vardır. Daha performanslı bir alternatif, Streamable'ı kullanmak olabilir: -

Ayrıca, bir Akış, Yinelenebilir veya Diziden oluşturulabilen ve birden çok kez yeniden oynatılabilen (tembel) bir Yayınlanabilir sınıf da vardır.

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

AsStreamable.synchronizedFromStream (stream) - iş parçacıkları arasında paylaşılabilecek bir şekilde yedekleme koleksiyonunu tembel bir şekilde dolduracak bir Streamable oluşturmak için kullanılabilir. Streamable.fromStream (stream) herhangi bir senkronizasyon ek yüküne neden olmaz.


2
Ve tabii ki, ortaya çıkan akışların önemli CPU / bellek ek yüküne ve çok zayıf paralel performansa sahip olduğuna dikkat edilmelidir. Ayrıca bu çözüm iş parçacığı açısından güvenli değildir (ortaya çıkan akışlardan birini başka bir iş parçacığına geçirip paralel olarak güvenli bir şekilde işleyemezsiniz). Çok daha performanslı ve güvenli olacaktır List<Integer> list = stream.collect(Collectors.toList()); streams = new Tuple2<>(list.stream(), list.stream())(OP'nin önerdiği gibi). Ayrıca lütfen cevabınızda cyclop akışlarının yazarı olduğunuzu açıkça belirtin. Bunu okuyun .
Tagir Valeev

Yazar olduğumu yansıtacak şekilde güncellendi. Ayrıca her birinin performans özelliklerini tartışmak için iyi bir nokta. Yukarıdaki değerlendirmeniz StreamUtils.duplicate için oldukça uygun. StreamUtils.duplicate, verileri bir Akıştan diğerine arabelleğe alarak çalışır ve hem CPU hem de Bellek ek yüküne neden olur (kullanım durumuna bağlı olarak). Ancak Streamable.of (1,2,3) için, her seferinde doğrudan Diziden yeni bir Akış oluşturulur ve paralel performans dahil olmak üzere performans özellikleri, normal olarak oluşturulan Akış ile aynı olacaktır.
John McClean

Ayrıca, bir Stream'den bir Streamable örneğinin oluşturulmasına izin veren, ancak oluşturulduğu anda Streamable'ı destekleyen koleksiyona erişimi senkronize eden bir AsStreamable sınıfı vardır (AsStreamable.synchronizedFromStream). İş parçacıkları arasında kullanım için daha uygun hale getirmek (ihtiyacınız olan şey buysa - Akışların% 99 oranında aynı iş parçacığında oluşturulduğunu ve yeniden kullanıldığını hayal ediyorum).
John McClean

Merhaba Tagir - Yorumunuzda rakip bir kütüphanenin yazarı olduğunuzu da belirtmeniz gerekmez mi?
John McClean

1
Yorumlar cevap değildir ve kitaplığımın akışı çoğaltma özelliği olmadığı için (sadece bunun işe yaramaz olduğunu düşündüğüm için) burada kitaplığımın reklamını yapmıyorum, bu yüzden burada rekabet etmiyoruz. Elbette, kütüphanemle ilgili bir çözüm önerdiğimde, her zaman açıkça yazar olduğumu söylüyorum.
Tagir Valeev

0

Bu özel sorun için bölümlemeyi de kullanabilirsiniz. Gibi bir şey

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

Stream Builder'ı bir akışı okurken veya yinelerken kullanabiliriz. İşte Stream Builder'ın belgesi .

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

Kullanım alanı

Diyelim ki çalışan akışımız var ve çalışan verilerini excel dosyasına yazmak için bu akışı kullanmamız ve ardından çalışan koleksiyonunu / tablosunu güncellememiz gerekiyor [Bu sadece Stream Builder'ın kullanımını göstermek için kullanım örneğidir]:

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

Benzer bir problemim vardı ve akışın bir kopyasını oluşturmak için üç farklı ara yapı düşünebilirdim: a List, bir dizi ve a Stream.Builder. Küçük bir kıyaslama programı yazdım ve performans açısından bakıldığındaList oldukça benzer olan diğer ikisinden yaklaşık% 30 daha yavaş olduğunu .

Bir diziye dönüştürmenin tek dezavantajı, öğe türünüzün genel bir tür olması (benim durumumda öyleydi); bu yüzden kullanmayı tercih ediyorumStream.Builder .

Sonunda aşağıdakileri oluşturan küçük bir işlev yazdım 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);
}

Sonra herhangi akışı bir kopyasını yapabilirsiniz stryaparak str.collect(copyCollector())oldukça akışların deyimsel kullanımında doğrultusunda hissediyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.