我正在尝试使用以下签名实现一个方法:
public static <A,B> Pair<Stream<A>, Stream<B>> flatten(Iterator<Pair<Stream<A>, Stream<B>>> iterator);
此方法的目标是将每个流类型展平为单个流并将输出包装成一对。我只有一个迭代器(不是Iterable),我不能改变方法签名,所以我必须在一次迭代中执行展平。
我目前最好的实施是
public static <A,B> Pair<Stream<A>, Stream<B>> flatten(Iterator<Pair<Stream<A>, Stream<B>> iterator) {
Stream<A> aStream = Stream.empty();
Stream<B> bStream = Stream.empty();
while(iterator.hasNext()) {
Pair<Stream<A>, Stream<B>> elm = iterator.next();
aStream = Stream.concat(aStream, elm.first);
bStream = Stream.concat(bStream, elm.second);
}
return Pair.of(aStream, bStream);
}
虽然这在技术上是正确的,但我对此并不十分满意,原因有两个:
感觉Stream#flatMap应该适合(在使用Guava's Streams.stream(Iterator)将输入Iterator转换为Stream之后,但由于中间的Pair类型,它似乎不起作用。
另外一个要求是任何迭代器/流可能非常大(例如,输入可能包含从一对极大的流到一个项目流中的许多流的任何地方),因此解决方案理想情况下不应包含收集结果进入内存中的集合。
答案 0 :(得分:10)
番石榴Streams.stream
没有魔力,它实际上只是内部:
StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
因此,当您可以直接使用它时,可能无需将其链接到您的方法。
你可以只使用Stream.Builder
:
public static <A, B> Pair<Stream<A>, Stream<B>> flatten(Iterator<Pair<Stream<A>, Stream<B>>> iterator) {
Stream.Builder<Stream<A>> builderA = Stream.builder();
Stream.Builder<Stream<B>> builderB = Stream.builder();
iterator.forEachRemaining(pair -> {
builderA.add(pair.first);
builderB.add(pair.second);
});
return Pair.of(builderA.build().flatMap(Function.identity()), builderB.build().flatMap(Function.identity()));
}
答案 1 :(得分:7)
避免收集整个Iterator
(就像你在问题中实际做的那样)是非常困难的,因为你不知道如何消费产生的流:一个可能被完全消耗,需要消费完全是迭代器,而另一个根本没有消耗,需要跟踪所有产生的对 - 有效地将它们收集到某个地方。
只有在&#34;速度&#34;或多或少地消耗流时,您才可以从不收集整个迭代器中受益。但是这样的消耗意味着要么使用其中一个结果流的迭代器,要么在并行线程中使用流 - 这需要额外的同步。
我因此建议将所有对收集到List
中,然后从该列表中生成新的Pair
:
public static <A,B> Pair<Stream<A>, Stream<B>> flatten(Iterator<Pair<Stream<A>, Stream<B>>> iterator) {
Iterable<Pair<Stream<A>, Stream<B>>> iterable = () -> iterator;
final List<Pair<Stream<A>, Stream<B>>> allPairs =
StreamSupport.stream(iterable.spliterator(), false)
.collect(Collectors.toList());
return Pair.of(
allPairs.stream().flatMap(p -> p.first),
allPairs.stream().flatMap(p -> p.second)
);
}
这不会消耗任何原始流,同时保留一个避免嵌套流连接的简单解决方案。
答案 2 :(得分:3)
首先,这将是一个更具功能性的&#34;你的代码版本,你说你更喜欢风格:
StackOverflowError
有关可能Stream.concat
的警告仍然适用于Iterator
。
为了避免这种情况并考虑大型数据集的性能和内存使用,我有以下建议(根本没有功能)。您可以创建一对自定义A
(适用于B
,Streams.stream()
类型)并使用Guava的iterator
来获取一对流。将这些自定义迭代器放在具有一对迭代器堆栈的类中。例如,如果在Stream<A>
中的第一对中,Stream<B>
的元素少于Stream<A>
,那么在iterator.next()
耗尽后,请调用B
并推送{的迭代器{1}}进入其堆栈。这是具有堆栈对的类(添加构造函数):
class PairStreamIterator<A, B> {
private final Iterator<Pair<Stream<A>, Stream<B>>> iterator;
private final Queue<Iterator<A>> stackA = new ArrayDeque<>();
private final Queue<Iterator<B>> stackB = new ArrayDeque<>();
Iterator<A> getItA() {
return new Iterator<A>() {
@Override public boolean hasNext() {
if (!stackA.isEmpty() && !stackA.peek().hasNext()) {
stackA.remove();
return hasNext();
} else if (!stackA.isEmpty() && stackA.peek().hasNext()) {
return true;
} else if (iterator.hasNext()) {
Pair<Stream<A>, Stream<B>> pair = iterator.next();
stackA.add(pair.first.iterator());
stackB.add(pair.second.iterator());
return hasNext();
}
return false;
}
@Override public A next() {
return stackA.peek().next();
}
};
}
// repeat for Iterator<B>
}
和flatten
方法:
<A, B> Pair<Stream<A>, Stream<B>> flattenIt(Iterator<Pair<Stream<A>, Stream<B>>> iterator) {
final PairStreamIterator<A, B> pair = new PairStreamIterator<>(iterator);
return Pair.of(Streams.stream(pair.getItA()), Streams.stream(pair.getItB()));
}
如果以相同的速率使用flatten
的结果对中的2个流,则2个堆栈通常会包含1个或2个迭代器。最糟糕的情况是,如果您计划完全使用结果对中的一个流,然后另一个。在这种情况下,第二个展平流所需的所有迭代器将保留在迭代器堆栈中。我不认为我有任何可怕的方法。由于这些存储在内存中的堆中,因此您仍然无法获得StackOverflowError
OutOfMemoryError
可能需要注意的是在hasNext
中使用递归。如果您在输入中遇到许多连续的空流,那只会是一个问题。