java - 你能将一个流分成两个流吗?
我有一个由Java 8流表示的数据集:
Stream stream = ...;
我可以看到如何过滤它以获得随机子集 - 例如
Random r = new Random();
PrimitiveIterator.OfInt coin = r.ints(0, 2).iterator();
Stream heads = stream.filter((x) -> (coin.nextInt() == 0));
我还可以看到如何减少此流以获取,例如,两个列表代表数据集的两个随机半部分,然后将它们转换回流。但是,有没有直接的方法从最初的流生成两个流? 就像是
(heads, tails) = stream.[some kind of split based on filter]
感谢您的任何见解。
9个解决方案
230 votes
可以使用收集器。
对于两个类别,请使用Stream工厂。
这将从IntStream到.collect(Collectors)创建Stream,并基于Predicate将项目放在一个或另一个列表中。
注意:由于流需要整体消耗,因此无法处理无限流。 因为无论如何都要使用流,所以此方法只是将它们放入列表中,而不是使用内存创建新的流。
此外,不需要迭代器,即使在您提供的仅限于头部的示例中也是如此。
Random r = new Random();
Map> groups = stream
.collect(Collectors.partitioningBy(x -> r.nextBoolean()));
System.out.println(groups.get(false).size());
System.out.println(groups.get(true).size());
有关更多类别,请使用Stream工厂。
Map> 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 p = x -> r.nextBoolean();
Map> groups = intStream.collect(() -> {
Map> map = new HashMap<>();
map.put(false, new ArrayList<>());
map.put(true, new ArrayList<>());
return map;
}, (map, x) -> {
boolean partition = p.test(x);
List 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 stream = intStream.boxed();
Mark Jeronimus answered 2019-07-03T08:36:14Z
18 votes
不幸的是,你要求的东西在JavaDoc of Stream中直接不受欢迎:
应该操作流(调用中间或终端 流操作)只有一次。 例如,这排除了&#34;分叉&#34; 流,相同的源提供两个或多个管道,或 多次遍历同一个流。
如果您真的希望这种行为,您可以使用Stream或其他方法解决此问题。 在这种情况下,您应该做的不是尝试使用分支过滤器从同一原始Stream源备份两个流,而是复制流并适当地过滤每个重复项。
但是,您可能希望重新考虑Stream是否适合您的用例。
Trevor Freeman answered 2019-07-03T08:37:07Z
13 votes
我偶然发现了这个问题,我觉得分叉的流有一些可以证明有效的用例。 我将以下代码作为消费者编写,以便它不会做任何事情,但您可以将其应用于函数和您可能遇到的任何其他内容。
class PredicateSplitterConsumer implements Consumer
{
private Predicate predicate;
private Consumer positiveConsumer;
private Consumer negativeConsumer;
public PredicateSplitterConsumer(Predicate predicate, Consumer positive, Consumer 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")));
LudgerP answered 2019-07-03T08:37:42Z
9 votes
不完全是。 你不能从中获得两个Streams; 这没有意义 - 如何在不需要同时生成另一个的情况下迭代一个? 流只能运行一次。
但是,如果要将它们转储到列表或其他内容中,则可以执行此操作
stream.forEach((x) -> ((x == 0) ? heads : tails).add(x));
Louis Wasserman answered 2019-07-03T08:34:48Z
7 votes
这违反了Stream的一般机制。 假设您可以将Stream S0拆分为Sa和Sb,就像您想要的那样。 在Sa上执行任何终端操作,例如count(),必然会消耗&#34; S0中的所有元素。 因此Sb丢失了它的数据源。
以前,我认为Stream有一个tee()方法,它将流复制到两个。 它现在被删除了。
Stream虽然有peek()方法,但您可以使用它来实现您的要求。
ZhongYu answered 2019-07-03T08:38:27Z
5 votes
不完全是这样,但您可以通过调用Collectors.groupingBy()来完成所需的操作。您可以创建一个新的Collection,然后可以在该新集合上实例化流。
aepurniet answered 2019-07-03T08:38:55Z
1 votes
这是我能想到的最不好的答案。
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
public class Test {
public static Pair splitStream(Stream inputStream, Predicate predicate,
Function, L> trueStreamProcessor, Function, R> falseStreamProcessor) {
Map> 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(trueResult, falseResult);
}
public static void main(String[] args) {
Stream stream = Stream.iterate(0, n -> n + 1).limit(10);
Pair, 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)
它并不理想,因为它将所有内容收集到打破流的中间集合中(并且有太多的参数!)
Ian Jones answered 2019-07-03T08:39:42Z
1 votes
我偶然发现了这个问题,同时寻找一种方法来过滤流中的某些元素并将其记录为错误。 所以我真的不需要拆分流,就像将一个过早的终止动作附加到具有不显眼语法的谓词一样。 这就是我想出的:
public class MyProcess {
/* Return a Predicate that performs a bail-out action on non-matching items. */
private static Predicate withAltAction(Predicate pred, Consumer altAction) {
return x -> {
if (pred.test(x)) {
return true;
}
altAction.accept(x);
return false;
};
/* Example usage in non-trivial pipeline */
public void processItems(Stream 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);
}
}
Sebastian Hans answered 2019-07-03T08:40:11Z
-2 votes
怎么样:
Supplier> randomIntsStreamSupplier =
() -> (new Random()).ints(0, 2).boxed();
Stream tails =
randomIntsStreamSupplier.get().filter(x->x.equals(0));
Stream heads =
randomIntsStreamSupplier.get().filter(x->x.equals(1));
Matthew answered 2019-07-03T08:40:34Z