ストリームを2つのストリームに分割できますか?


146

Java 8ストリームで表されるデータセットがあります。

Stream<T> stream = ...;

それをフィルタリングしてランダムなサブセットを取得する方法を見ることができます-たとえば

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

また、このストリームを削減して、たとえばデータセットの2つのランダムな半分を表す2つのリストを取得し、それらをストリームに戻す方法も確認できます。しかし、最初のストリームから2つのストリームを生成する直接的な方法はありますか?何かのようなもの

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

洞察をありがとう。


マークの答えはルイスの答えよりもはるかに役に立ちますが、ルイスの答えは元の質問により関連していると私は言わなければなりません。質問は、中間変換なしでStream複数Streamのに変換する可能性に焦点を当てていますが、この質問に到達した人々は、マークの答えであるそのような制約に関係なく、実際に達成する方法を探していると思います。これは、タイトルの質問が説明の質問と同じではないためと考えられます
devildelta

回答:


9

ではない正確に。Stream1つから2つを取得することはできません。これは意味がありません-同時にもう1つを生成する必要なく、どのように1つを反復しますか?ストリームは一度だけ操作できます。

ただし、それらをリストなどにダンプしたい場合は、

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

65
なぜ意味がないのですか?ストリームはパイプラインであるため、元のストリームの2つのプロデューサーを作成できなかった理由はないので、2つのストリームを提供するコレクターによってこれが処理されているのがわかります。
ブレットライアン

36
スレッドセーフではありません。コレクションに直接追加しようとする悪いアドバイスです。これは、stream.collect(...)事前定義されたスレッドセーフのfor があり、Collectorsスレッドセーフでないコレクション(同期ロックの競合がない)でもうまく機能する理由です。@MarkJeronimusによるベストアンサー。
ヨーヨー

1
@JoDヘッドとテールがスレッドセーフであれば、スレッドセーフです。さらに、非並列ストリームの使用を想定すると、順序のみが保証されないため、スレッドセーフです。並行性の問題を修正するのはプログラマ次第なので、コレクションがスレッドセーフである場合、この回答は完全に適しています。
Nicolas

1
@Nixonそれは私たちがここに持っているより良いソリューションの存在下では適していません。そのようなコードがあると、悪い先例につながり、他の人がそれを間違った方法で使用することになります。並列ストリームが使用されていない場合でも、わずか1ステップです。適切なコーディング方法では、ストリーム操作中に状態を維持しないようにする必要があります。次に行うのは、Apache sparkのようなフレームワークでコーディングすることです。同じプラクティスを行うと、予期しない結果が実際に発生します。それは独創的な解決策でした。私がそう言ったのは、私がそれほど前に自分で書いたのではないでしょうか。
YoYo

1
@JoDこれは優れたソリューションではなく、事実上非効率的です。この考え方は、最終的にすべてのコレクションがデフォルトでスレッドセーフであり、意図しない結果を防ぐ必要があるという結論に終わります。
Nicolas

301

これにはコレクターを使用できます。

  • 2つのカテゴリの場合は、Collectors.partitioningBy()factoryを使用します。

これによりMapfrom が作成さBooleanList、に基づいてアイテムがいずれかのリストに配置されますPredicate

注:ストリーム全体を消費する必要があるため、これは無限ストリームでは機能しません。とにかくストリームが消費されるので、このメソッドは、メモリ付きの新しいストリームを作成するのではなく、単にリストに入れます。出力としてストリームが必要な場合は、いつでもこれらのリストをストリーミングできます。

また、提供したヘッドのみの例でも、イテレータは必要ありません。

  • バイナリ分割は次のようになります。
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());
  • その他のカテゴリについては、Collectors.groupingBy()ファクトリを使用してください。
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());

ストリームがではなくStream、のようなプリミティブストリームの1つである場合、IntStreamこの.collect(Collectors)メソッドは使用できません。あなたはコレクターファクトリーなしで手動でそれをしなければならないでしょう。その実装は次のようになります。

[2020-04-16以降の例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 ));
            });

この例では、最初のコレクションのフルサイズでArrayListを初期化します(これがまったくわかっている場合)。これにより、最悪の場合でもイベントのサイズ変更が防止されますが、2 * N * Tのスペース(N =要素の初期数、T =スレッド数)が乱れる可能性があります。スペースと速度をトレードオフするには、スペースを省略するか、1つのパーティションで予想される要素の最大数(通常、バランスの取れた分割の場合はN / 2を超える)など、経験に基づいた推測を使用します。

私はJava 9メソッドを使用して他人を怒らせないことを望みます。Java 8バージョンについては、編集履歴をご覧ください。


2
綺麗な。ただし、並列化されたストリームの場合、IntStreamの最後のソリューションはスレッドセーフではありません。解決策はあなたが思っているよりはるかに簡単です... stream.boxed().collect(...);!それは宣伝どおりに行われます:プリミティブIntStreamをボックスStream<Integer>版に変換します。
YoYo

32
これはOPの質問を直接解決するため、受け入れられる答えになるはずです。
ejel

27
Stack Overflowを使用して、より良い回答が見つかった場合にコミュニティが選択した回答を上書きできるようにしたいと思います。
GuiSim

これで質問の答えがわかりません。質問は、ストリームをリストではなくストリームに分割することを要求します。
AlikElzin-kilaka

1
アキュムレータ関数は不必要に冗長です。代わりに、(map, x) -> { boolean partition = p.test(x); List<Integer> list = map.get(partition); list.add(x); }単にを使用できます(map, x) -> map.get(p.test(x)).add(x)。さらに、collect操作がスレッドセーフであってはならない理由はわかりません。これは、想定されているとおりに機能し、どのようCollectors.partitioningBy(p)に機能するかに非常に密接に機能します。ただし、ボクシングを2回回避するために、を使用しない場合は、IntPredicate代わりにを使用します。Predicate<Integer>boxed()
ホルガー

21

私はこの質問に出くわしましたが、分岐したストリームには有効であると証明できるいくつかのユースケースがあると感じています。以下のコードはコンシューマーとして作成したので、何も実行しませんが、関数や他のあらゆるものに適用できます。

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

コードの実装は次のようになります。

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

残念ながら、あなたが求めるものは、StreamJavaDocで直接嫌がられています:

ストリームの操作(中間または最終ストリーム操作の呼び出し)は1回だけです。これは、たとえば、同じソースが2つ以上のパイプラインにフィードする「分岐」ストリーム、または同じストリームの複数のトラバーサルを除外します。

このpeek種の動作が本当に必要な場合は、この方法または他の方法で回避できます。この場合、フォークフィルターを使用して同じ元のStreamソースから2つのストリームを戻そうとする代わりに、ストリームを複製して、それぞれの複製を適切にフィルタリングします。

ただし、a Streamがユースケースに適した構造であるかどうかを再検討することをお勧めします。


6
長い単一ストリーム項目のみに行くようにとjavadocの文言は、複数のストリームに分割排除するものではなく、1これらの
するThorbjörnRavnアンデルセン

2
@ThorbjørnRavnAndersenストリームアイテムの複製が分岐したストリームの主な障害になるかどうかはわかりません。主な問題は、フォーク操作は基本的にターミナル操作であるため、フォークすることを決定すると、基本的にはある種のコレクションが作成されます。たとえば、メソッドを書くことはできますList<Stream> forkStream(Stream s)が、結果のストリームは少なくとも部分的にはコレクションによってバッキングされ、基になるストリームによって直接バッキングされることfilterはありません。
Trevor Freeman

7
これは私がJavaのストリームがあると感じ理由の一つであるビットに比べ中途半端github.com/ReactiveX/RxJava/wikiストリームのポイントが頻繁に分割を必要要素と現実世界の事業の潜在的無限集合に対して操作を適用することですので、 、ストリームの複製とマージ。
Usman Ismail

8

これは、Streamの一般的なメカニズムに反しています。ストリームS0をSaとSbに好きなように分割できるとします。count()Saで端末操作を実行すると、S0のすべての要素が必ず「消費」されます。したがって、Sbはデータソースを失いました。

以前は、Streamにはtee()ストリームを2つに複製するメソッドがあったと思います。現在は削除されています。

Streamにはpeek()メソッドがありますが、これを使用して要件を達成できる場合があります。


1
peek以前とまったく同じteeです。
Louis Wasserman

5

正確ではありませんが、を呼び出すことで、必要なことを実行できる場合がありますCollectors.groupingBy()。新しいコレクションを作成すると、その新しいコレクションでストリームをインスタンス化できます。


2

これは私が思いつくことができる最も悪い答えでした。

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

}

これは整数のストリームを受け取り、それらを5で分割します。5を超えるものについては、偶数のみをフィルタリングしてリストに入れます。残りは、|で結合します。

出力:

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

ストリームを壊す中間コレクションにすべてを収集するため(そして引数が多すぎるため)、理想的ではありません。


1

ストリームから特定の要素をフィルタリングしてエラーとしてログに記録する方法を探しているときに、私はこの質問を見つけました。そのため、ストリームを分割する必要はありませんでした。途中で終了するアクションを控えめな構文で述語にアタッチする必要はありませんでした。これは私が思いついたものです:

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

ロンボクを使用する短いバージョン

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

どうですか:

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
サプライヤーは2回呼び出されるため、2つの異なるランダムコレクションを取得します。私はそれがで追いついからオッズを分割するOPの心だと思う同じ生成されたシーケンス
USR-ローカルΕΨΗΕΛΩΝ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.