Bir akışı iki akıma bölebilir misiniz?


146

Bir Java 8 akışı ile temsil edilen bir veri kümesi var:

Stream<T> stream = ...;

Rastgele bir altküme almak için nasıl filtreleneceğini görebilirim - örneğin

Random r = new Random();
PrimitiveIterator.OfInt coin = r.ints(0, 2).iterator();   
Stream<T> heads = stream.filter((x) -> (coin.nextInt() == 0));

Ayrıca, veri kümesinin iki rasgele yarısını temsil eden iki listeyi almak için bu akışı nasıl azaltabileceğimi de görebilirim ve sonra bunları tekrar akışlara dönüştürebilirim. Ancak, ilkinden iki akış oluşturmanın doğrudan bir yolu var mı? Gibi bir şey

(heads, tails) = stream.[some kind of split based on filter]

Herhangi bir görüş için teşekkürler.


Mark'ın cevabı Louis'in cevabından çok yararlıdır, ancak Louis'in orijinal soru ile daha ilgili olduğunu söylemeliyim. Soru, orta dönüşüm olmadanStream birden çok Streams'ye dönüştürme olasılığına odaklanmış olsa da, bu soruya ulaşan insanların aslında Mark'ın cevabı olan bu kısıtlamadan bağımsız olarak bu yolu elde etmenin yolunu aradıklarını düşünüyorum. Bu , başlıktaki sorunun açıklamadakiyle aynı olmaması nedeniyle olabilir .
devildelta

Yanıtlar:


9

Tam olarak değil. Birinden iki tane alamazsınız Stream; bu mantıklı değil - diğerini aynı anda üretmeye gerek kalmadan birinden nasıl tekrar ederdiniz? Bir akış yalnızca bir kez çalıştırılabilir.

Ancak, bunları bir listeye veya başka bir şeye dökmek istiyorsanız,

stream.forEach((x) -> ((x == 0) ? heads : tails).add(x));

65
Neden mantıklı değil? Bir akış bir boru hattı olduğundan, orijinal akışın iki üreticisini oluşturamamasının bir nedeni yoktur, bunun iki akış sağlayan bir koleksiyoncu tarafından ele alındığını görebiliyordum.
Brett Ryan

36
Güvenli değil. Doğrudan bir koleksiyona eklemeye çalışan kötü tavsiye, bu nedenle stream.collect(...)önceden tanımlanmış bir iş parçacığı için güvenli Collectorsolan, iş parçacığı için güvenli olmayan Koleksiyonlarda bile (senkronize kilit çekişmesi olmadan) iyi çalışan for. @MarkJeronimus tarafından en iyi cevap.
YoYo

1
@JoD Kafalar ve kuyruklar diş açmaya karşı emniyetlidir. Buna ek olarak, paralel olmayan akışların kullanıldığı varsayılırsa, yalnızca sipariş garanti edilmez, bu nedenle iş parçacığı açısından güvenlidir. Eşzamanlılık sorunlarını gidermek programcıya kalmıştır, bu nedenle koleksiyonlar iş parçacığı açısından güvenliyse bu cevap mükemmel bir şekilde uygundur.
Nicolas

1
@Nixon, burada sahip olduğumuz daha iyi bir çözüm varlığında uygun değildir. Böyle bir koda sahip olmak kötü bir emsal oluşturabilir ve başkalarının yanlış bir şekilde kullanmasına neden olabilir. Paralel akış kullanılmasa bile, sadece bir adım uzaklıktadır. İyi kodlama uygulamaları, akış işlemleri sırasında durumu korumamamızı gerektirir. Yaptığımız bir sonraki şey Apache spark gibi bir çerçevede kodlamak ve aynı uygulamalar gerçekten beklenmedik sonuçlara yol açacak. Bu yaratıcı bir çözümdü, ben de kendimi çok uzun zaman önce yazmamış olabileceğimi söylüyorum.
YoYo

1
@JoD Bu daha iyi bir çözüm değil, aslında daha verimsiz.
Nicolas

301

Bunun için bir toplayıcı kullanılabilir.

  • İki kategori için Collectors.partitioningBy()fabrika kullanın .

Bu, bir Mapfrom Booleanila oluşturur Listve öğeleri a veya diğer listeye a Predicate.

Not: Akışın bütün olarak tüketilmesi gerektiğinden, bu sonsuz akışlarda çalışamaz. Akış yine de tüketildiği için, bu yöntem onları bellekle yeni bir akış yapmak yerine listelere koyar. Çıktı olarak akışa ihtiyacınız varsa bu listeleri her zaman aktarabilirsiniz.

Ayrıca, yinelemeye gerek yok, sağladığınız sadece kafa örneğinde bile.

  • İkili bölme şöyle görünür:
Random r = new Random();

Map<Boolean, List<String>> groups = stream
    .collect(Collectors.partitioningBy(x -> r.nextBoolean()));

System.out.println(groups.get(false).size());
System.out.println(groups.get(true).size());
  • Daha fazla kategori için bir Collectors.groupingBy()fabrika kullanın .
Map<Object, List<String>> groups = stream
    .collect(Collectors.groupingBy(x -> r.nextInt(3)));
System.out.println(groups.get(0).size());
System.out.println(groups.get(1).size());
System.out.println(groups.get(2).size());

Akarsuların olmaması Stream, ancak ilkel akarsulardan biri olması durumunda IntStream, bu .collect(Collectors)yöntem mevcut değildir. Bir kollektör fabrikası olmadan manuel olarak yapmanız gerekecek. Bu uygulama şöyle görünür:

[Örnek 2020-04-16'dan beri 2.0]

    IntStream    intStream = IntStream.iterate(0, i -> i + 1).limit(100000).parallel();
    IntPredicate predicate = ignored -> r.nextBoolean();

    Map<Boolean, List<Integer>> groups = intStream.collect(
            () -> Map.of(false, new ArrayList<>(100000),
                         true , new ArrayList<>(100000)),
            (map, value) -> map.get(predicate.test(value)).add(value),
            (map1, map2) -> {
                map1.get(false).addAll(map2.get(false));
                map1.get(true ).addAll(map2.get(true ));
            });

Bu örnekte ArrayLists'i ilk koleksiyonun tam boyutuyla başlatırım (bu zaten biliniyorsa). Bu, en kötü senaryoda bile yeniden boyutlandırma olaylarını önler, ancak potansiyel olarak 2 * N * T alanını artırabilir (N = ilk öğe sayısı, T = iş parçacığı sayısı). Alanı hız için takas etmek için, bir bölümdeki beklenen en yüksek sayıda öğe gibi (genellikle dengeli bir bölünme için N / 2'nin üzerinde) olduğu gibi, dışarıda bırakabilir veya en iyi eğitimli tahmininizi kullanabilirsiniz.

Umarım bir Java 9 yöntemi kullanarak kimseyi incitmezim. Java 8 sürümü için düzenleme geçmişine bakın.


2
Güzel. Ancak, IntStream için son çözüm, paralelleştirilmiş bir akış durumunda iş parçacığı için güvenli olmayacaktır. Çözüm sandığınızdan çok daha basit ... stream.boxed().collect(...);! Reklamı yapılan şekilde yapılacaktır: ilkel IntStreamolanı kutulu Stream<Integer>sürüme dönüştürün .
YoYo

32
Bu, OP sorusunu doğrudan çözdüğü için kabul edilen cevap olmalıdır.
ejel

27
Yığın Taşması'nın, daha iyi bir yanıt bulunduğunda topluluğun seçilen yanıtı geçersiz kılmasına izin vermesini isterdim.
GuiSim

Bunun soruyu cevapladığından emin değilim. Soru, bir akışın Listeler'e değil akışlara bölünmesini ister.
AlikElzin-kilaka

1
Akümülatör işlevi gereksiz yere ayrıntılıdır. Bunun yerine (map, x) -> { boolean partition = p.test(x); List<Integer> list = map.get(partition); list.add(x); }sadece kullanabilirsiniz (map, x) -> map.get(p.test(x)).add(x). Ayrıca, collectişlemin iş parçacığı için güvenli olmaması için herhangi bir neden göremiyorum . Tam olarak çalışması gerektiği gibi ve nasıl Collectors.partitioningBy(p)çalışacağına çok yakın çalışır. Ama iki kere boks yapmaktan kaçınmak için kullanmıyorum IntPredicateyerine kullanmak istiyorum . Predicate<Integer>boxed()
Holger

21

Bu soruya kendimi tökezledim ve çatallı bir akışın geçerli olabilecek bazı kullanım durumları olduğunu hissediyorum. Tüketici olarak aşağıdaki kodu yazdım, böylece hiçbir şey yapmaz, ancak işlevlere ve karşılaşabileceğiniz başka bir şeye uygulayabilirsiniz.

class PredicateSplitterConsumer<T> implements Consumer<T>
{
  private Predicate<T> predicate;
  private Consumer<T>  positiveConsumer;
  private Consumer<T>  negativeConsumer;

  public PredicateSplitterConsumer(Predicate<T> predicate, Consumer<T> positive, Consumer<T> negative)
  {
    this.predicate = predicate;
    this.positiveConsumer = positive;
    this.negativeConsumer = negative;
  }

  @Override
  public void accept(T t)
  {
    if (predicate.test(t))
    {
      positiveConsumer.accept(t);
    }
    else
    {
      negativeConsumer.accept(t);
    }
  }
}

Şimdi kod uygulamanız şu şekilde olabilir:

personsArray.forEach(
        new PredicateSplitterConsumer<>(
            person -> person.getDateOfBirth().isPresent(),
            person -> System.out.println(person.getName()),
            person -> System.out.println(person.getName() + " does not have Date of birth")));

20

Ne yazık ki, doğrudan JavaDoc Akışında istediğiniz şey kaşlarını çatıyor :

Bir akış yalnızca bir kez çalıştırılmalıdır (bir ara veya terminal akış işlemini başlatır). Bu, örneğin, aynı kaynağın iki veya daha fazla boru hattını veya aynı akışın birden fazla geçişini beslediği "çatallı" akışları ortadan kaldırır.

Bunu kullanarak bu sorunu çözebilirsiniz. peek tür davranışları gerçekten arzu ederseniz, veya diğer yöntemleri . Bu durumda, aynı orijinal Akış kaynağından iki akışı çatal filtresiyle yedeklemeye çalışmak yerine, akışınızı çoğaltır ve kopyaların her birine uygun şekilde filtre uygularsınız.

Ancak, Streamkullanım durumunuz için a'nın uygun yapı olup olmadığını yeniden düşünmek isteyebilirsiniz .


6
Uzun bir tek akış öğesi yalnızca gider gibi javadoc ifadeler çeşitli akışlarına bölünmesine dışlamaz biri Bunlardan
Thorbjorn Ravn Andersen

2
@ ThorbjørnRavnAndersen Bir akış öğesinin çoğaltılmasının çatallı bir akışın en büyük engeli olduğundan emin değilim. Ana sorun, çatal işleminin esas olarak bir terminal işlemi olmasıdır, bu nedenle çatallamaya karar verdiğinizde temelde bir çeşit koleksiyon oluşturursunuz. Örneğin, bir yöntem yazabilirim, List<Stream> forkStream(Stream s)ancak sonuçta elde edilen akışlarım, en azından kısmen koleksiyonlar tarafından desteklenecek ve doğrudan bir akım akışı tarafından değil, filterhangisinin bir terminal akışı işlemi olmadığını söyleyeceğiz .
Trevor Freeman

7
Bu, Java akışlarının github.com/ReactiveX/RxJava/wiki ile karşılaştırıldığında biraz yarı yamalak olduğunu hissetmemizin nedenlerinden biri, çünkü akış noktası potansiyel olarak sonsuz bir öğe kümesine işlemleri uygulamak ve gerçek dünya işlemleri sıklıkla bölünmeyi gerektiriyor , çoğaltma ve birleştirme akışları.
Usman Ismail

8

Bu, Stream'in genel mekanizmasına aykırıdır. S0 Akışını Sa ve Sb'ye istediğiniz gibi bölebileceğinizi varsayalım. count()Sa üzerinde herhangi bir terminal işleminin gerçekleştirilmesi, mutlaka S0'daki tüm elemanları "tüketecektir". Bu nedenle Sb veri kaynağını kaybetti.

Daha önce, Stream'in tee()bir akışı ikiye kopyalayan bir yöntemi vardı . Şimdi kaldırıldı.

Stream bir peek () yöntemine sahiptir, ancak gereksinimlerinize ulaşmak için bu yöntemi kullanabilirsiniz.


1
peekeskiden olduğu gibi tee.
Louis Wasserman

5

tam olarak değil, ama ihtiyaç duyduğunuz şeyi çağırarak başarabilirsiniz Collectors.groupingBy(). yeni bir Koleksiyon oluşturursunuz ve daha sonra bu yeni koleksiyondaki akışları başlatabilirsiniz.


2

Bu, karşılaşabileceğim en az kötü cevaptı.

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

public class Test {

    public static <T, L, R> Pair<L, R> splitStream(Stream<T> inputStream, Predicate<T> predicate,
            Function<Stream<T>, L> trueStreamProcessor, Function<Stream<T>, R> falseStreamProcessor) {

        Map<Boolean, List<T>> partitioned = inputStream.collect(Collectors.partitioningBy(predicate));
        L trueResult = trueStreamProcessor.apply(partitioned.get(Boolean.TRUE).stream());
        R falseResult = falseStreamProcessor.apply(partitioned.get(Boolean.FALSE).stream());

        return new ImmutablePair<L, R>(trueResult, falseResult);
    }

    public static void main(String[] args) {

        Stream<Integer> stream = Stream.iterate(0, n -> n + 1).limit(10);

        Pair<List<Integer>, String> results = splitStream(stream,
                n -> n > 5,
                s -> s.filter(n -> n % 2 == 0).collect(Collectors.toList()),
                s -> s.map(n -> n.toString()).collect(Collectors.joining("|")));

        System.out.println(results);
    }

}

Bu bir tamsayı akışı alır ve 5'de böler. 5'ten büyük olanlar için sadece çift sayıları filtreler ve bir listeye koyar. Geri kalanı için onları |.

çıktılar:

 ([6, 8],0|1|2|3|4|5)

Her şeyi akışı kesen ara koleksiyonlara topladığı için ideal değil (ve çok fazla argüman var!)


1

Bazı öğeleri akıştan filtrelemenin ve hata olarak kaydetmenin bir yolunu ararken bu soruya rastladım. Bu yüzden akışı, göze batmayan sözdizimine sahip bir yükleme erken bir sonlandırma eylemi eklemek kadar çok bölmem gerekmiyordu. Ben geldi budur:

public class MyProcess {
    /* Return a Predicate that performs a bail-out action on non-matching items. */
    private static <T> Predicate<T> withAltAction(Predicate<T> pred, Consumer<T> altAction) {
    return x -> {
        if (pred.test(x)) {
            return true;
        }
        altAction.accept(x);
        return false;
    };

    /* Example usage in non-trivial pipeline */
    public void processItems(Stream<Item> stream) {
        stream.filter(Objects::nonNull)
              .peek(this::logItem)
              .map(Item::getSubItems)
              .filter(withAltAction(SubItem::isValid,
                                    i -> logError(i, "Invalid")))
              .peek(this::logSubItem)
              .filter(withAltAction(i -> i.size() > 10,
                                    i -> logError(i, "Too large")))
              .map(SubItem::toDisplayItem)
              .forEach(this::display);
    }
}

0

Lombok kullanan daha kısa versiyon

import java.util.function.Consumer;
import java.util.function.Predicate;

import lombok.RequiredArgsConstructor;

/**
 * Forks a Stream using a Predicate into postive and negative outcomes.
 */
@RequiredArgsConstructor
@FieldDefaults(makeFinal = true, level = AccessLevel.PROTECTED)
public class StreamForkerUtil<T> implements Consumer<T> {
    Predicate<T> predicate;
    Consumer<T> positiveConsumer;
    Consumer<T> negativeConsumer;

    @Override
    public void accept(T t) {
        (predicate.test(t) ? positiveConsumer : negativeConsumer).accept(t);
    }
}

-3

Nasıl olur:

Supplier<Stream<Integer>> randomIntsStreamSupplier =
    () -> (new Random()).ints(0, 2).boxed();

Stream<Integer> tails =
    randomIntsStreamSupplier.get().filter(x->x.equals(0));
Stream<Integer> heads =
    randomIntsStreamSupplier.get().filter(x->x.equals(1));

1
Tedarikçi iki kez çağrıldığından, iki farklı rastgele koleksiyon alacaksınız. Bunun içinde eşitler gelen oran bölmek OP'ın zihni olduğunu düşünüyorum aynı oluşturulan dizisi
usr-yerel-ΕΨΗΕΛΩΝ
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.