你能将一个流分成两个流吗?

时间:2013-11-12 21:31:20

标签: java java-8 java-stream

我有一个由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]

感谢您的任何见解。

11 个答案:

答案 0 :(得分:234)

可以使用收集器

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

这将从MapBoolean创建List,并根据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,而是IntStream之类的原始流,则此.collect(Collectors)方法不可用。没有收集器工厂,您必须以手动方式完成。它的实现如下:

IntStream intStream = IntStream.iterate(0, i -> i + 1).limit(1000000);

Predicate<Integer> p = x -> r.nextBoolean();
Map<Boolean, List<Integer>> groups = intStream.collect(() -> {
    Map<Boolean, List<Integer>> map = new HashMap<>();
    map.put(false, new ArrayList<>());
    map.put(true, new ArrayList<>());
    return map;
}, (map, x) -> {
    boolean partition = p.test(x);
    List<Integer> list = map.get(partition);
    list.add(x);
}, (map1, map2) -> {
    map1.get(false).addAll(map2.get(false));
    map1.get(true).addAll(map2.get(true));
});

System.out.println(groups.get(false).size());
System.out.println(groups.get(true).size());

修改

正如所指出的,以上的解决方法&#39;不是线程安全的。在收集之前转换为正常Stream是要走的路:

Stream<Integer> stream = intStream.boxed();

答案 1 :(得分:18)

不幸的是,你要求的内容在JavaDoc of Stream

中直接受到了谴责
  

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

如果你真的希望这种行为,你可以使用peek或其他方法解决这个问题。在这种情况下,您应该做的不是尝试使用分支过滤器从同一原始Stream源备份两个流,而是复制流并适当地过滤每个重复项。

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

答案 2 :(得分:14)

我偶然发现了这个问题,我觉得分叉的流有一些可以证明有效的用例。我将下面的代码作为消费者编写,以便它不会做任何事情,但你可以将它应用于函数和你可能遇到的任何其他内容。

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

答案 3 :(得分:9)

不完全是。你不能从中获得两个Stream;这没有意义 - 如何在不需要同时生成另一个的情况下迭代一个?流只能运行一次。

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

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

答案 4 :(得分:7)

这违反了Stream的一般机制。假设您可以将Stream S0拆分为Sa和Sb,就像您想要的那样。在Sa上执行任何终端操作,例如count(),必然会“消耗”S0中的所有元素。因此,Sb丢失了它的数据源。

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

Stream有一个peek()方法,你或许可以用它来达到你的要求。

答案 5 :(得分:5)

并不完全,但您可以通过调用Collectors.groupingBy()来完成所需的操作。您创建一个新的Collection,然后可以在该新集合上实例化流。

答案 6 :(得分:1)

这是我能想出的最不好的答案。

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)

它不理想,因为它将所有内容收集到打破流的中间集合中(并且有太多的参数!)

答案 7 :(得分: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);
    }
}

答案 8 :(得分:1)

可以得到两个 Stream一个
从带有 teeing
的 Java 12 开始 计算 100 次抛硬币的正面和反面

Random r = new Random();
PrimitiveIterator.OfInt coin = r.ints(0, 2).iterator();
List<Long> list = Stream.iterate(0, i -> coin.nextInt())
    .limit(100).collect(teeing(
        filtering(i -> i == 1, counting()),
        filtering(i -> i == 0, counting()),
        (heads, tails) -> {
          return(List.of(heads, tails));
        }));
System.err.println("heads:" + list.get(0) + " tails:" + list.get(1));

获取例如:heads:51 tails:49

答案 9 :(得分: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);
    }
}

答案 10 :(得分:-2)

怎么样:

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