您可以将一个流分成两个流吗?


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

我还可以看到如何减少该流,例如,获得两个表示数据集的两个随机一半的列表,然后将它们转换回流。但是,是否有直接方法可以从最初的一个生成两个流?就像是

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

感谢您的任何见解。


马克的答案比路易斯的答案有用得多,但我必须说路易斯的答案与原始问题更相关。这个问题主要集中在不经过中间转换就可以转换StreamStreams 的可能性上,尽管我认为达到此问题的人实际上正在寻求实现这一目标的方法,而不管这种约束如何,这是Mark的答案。这可能是由于标题的问题与说明中的问题不同
devildelta

Answers:


9

不完全是。您不可能一分之二Stream。这没有道理-您将如何遍历一个而不需要同时生成另一个?流只能操作一次。

但是,如果要将它们转储到列表或其他内容中,则可以执行

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

65
为什么没有道理?由于流是管道,因此没有理由不能创建原始流的两个生产者,我可以看到这是由提供两个流的收集器处理的。
Brett Ryan

36
不是线程安全的。尝试直接添加到集合中的错误建议,这就是为什么我们拥有stream.collect(...)带有预定义线程安全的for的原因Collectors,即使在非线程安全的集合(没有同步锁争用)上也能很好地工作。@MarkJeronimus的最佳答案。
2015年

1
@JoD如果头部和尾部是线程安全的,则是线程安全的。另外,假设使用非并行流,则不能保证仅顺序,因此它们是线程安全的。由程序员来解决并发问题,因此,如果集合是线程安全的,则此答案非常适合。
尼古拉斯

1
@Nixon在存在更好的解决方案的情况下不适合使用,我们在这里提供了更好的解决方案。拥有这样的代码可能会导致错误的先例,导致其他人以错误的方式使用它。即使不使用并行流,也只有一步之遥。良好的编码习惯要求我们在流操作期间不要保持状态。我们要做的下一步是在Apache Spark之类的框架中进行编码,同样的做法实际上会导致意外的结果。我提供了一种创新的解决方案,我可能不久前就写了一个自己的解决方案。
悠悠球

1
@JoD这不是一个更好的解决方案,实际上效率更高。这种想法最终得出的结论是,默认情况下,所有Collections应该是线程安全的,以防止意外的结果,这是错误的。
尼古拉斯

301

集电极可以用于此目的。

  • 对于两个类别,请使用Collectors.partitioningBy()工厂。

这将创建MapBoolean到的List条目,并根据条目将项目放在一个或另一个列表中Predicate

注意:由于流需要全部消耗掉,因此无法在无限流上使用。而且由于无论如何都会消耗流,因此此方法只是将它们放入列表中,而不是创建新的带内存流。如果需要流作为输出,则始终可以流式处理这些列表。

另外,甚至在您提供的仅heads示例中也不需要迭代器。

  • 二进制拆分看起来像这样:
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,而是原始流之一(例如)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 =线程数)。要权衡速度,您可以不使用它,也可以使用受过良好教育的猜测,例如一个分区中预期的最大元素数(对于平衡的拆分,通常刚好超过N / 2)。

我希望我不会使用Java 9方法来冒犯任何人。对于Java 8版本,请查看编辑历史记录。


2
美丽。但是,在并行流的情况下,IntStream的最后一种解决方案将不是线程安全的。解决方案比您认为的简单得多stream.boxed().collect(...);!它将按照广告中的说明进行操作:将原语转换IntStream为盒装Stream<Integer>版本。
2015年

32
这应该是可以接受的答案,因为它直接解决了OP问题。
ejel

27
我希望如果发现更好的答案,堆栈溢出将使社区能够覆盖选定的答案。
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)。但我会使用IntPredicate而不是Predicate<Integer>不使用boxed()来避免装箱两次。
Holger

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中直接被皱眉:

流只能操作一次(调用中间流或终端流操作)。例如,这排除了“分叉”流,其中,同一源提供了两个或多个管道,或同一流的多次遍历。

如果peek您确实想要这种行为,则可以使用其他方法解决此问题。在这种情况下,您应该做的是代替使用分叉过滤器从同一原始Stream源备份两个流,而是复制流并适当地过滤每个重复的流。

但是,您可能希望重新考虑a Stream是否适合您的用例。


6
javadoc的措辞并不排除划分成几个流,只要单流选项只有在进入一个这些
托尔比约恩Ravn的安德森

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丢失了其数据源。

tee()我认为以前,Stream具有一种将流复制为两个的方法。现在已删除。

尽管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
由于供应商被两次调用,您将获得两个不同的随机集合。我认为这是OP的头脑分裂从唇上的赔率在相同生成的序列
USR-本地ΕΨΗΕΛΩΝ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.