如何将迭代器转换为流?

615

我正在寻找一种简洁的方法将一个 Iterator 转换为 Stream,或者更具体地说,将迭代器视为流进行处理。

出于性能的考虑,我希望避免在新列表中复制迭代器:

Iterator<String> sourceIterator = Arrays.asList("A", "B", "C").iterator();
Collection<String> copyList = new ArrayList<String>();
sourceIterator.forEachRemaining(copyList::add);
Stream<String> targetStream = copyList.stream();
根据评论中的一些建议,我也尝试使用Stream.generate
public static void main(String[] args) throws Exception {
    Iterator<String> sourceIterator = Arrays.asList("A", "B", "C").iterator();
    Stream<String> targetStream = Stream.generate(sourceIterator::next);
    targetStream.forEach(System.out::println);
}

然而,我遇到了一个NoSuchElementException(因为没有调用hasNext

Exception in thread "main" java.util.NoSuchElementException
    at java.util.AbstractList$Itr.next(AbstractList.java:364)
    at Main$$Lambda$1/1175962212.get(Unknown Source)
    at java.util.stream.StreamSpliterators$InfiniteSupplyingSpliterator$OfRef.tryAdvance(StreamSpliterators.java:1351)
    at java.util.Spliterator.forEachRemaining(Spliterator.java:326)
    at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
    at Main.main(Main.java:20)

我查看了StreamSupportCollections,但没有找到任何内容。


5
可能是重复的问题:如何从一个迭代器创建一个无限流(Stream<E>)? - Dmitry Ginzburg
3
@DmitryGinzburg 嗯,我不想创建一个“无限”的流。 - gontard
@gontard 在这种情况下那并不重要。 - Dmitry Ginzburg
2
@DmitryGinzburg Stream.generate(iterator::next) 能行吗? - gontard
1
@DmitryGinzburg 那对于有限迭代器是行不通的。 - assylias
8
请参见https://dev59.com/TWAg5IYBdhLWcg3w1t9a#23177907。该链接中讨论了为什么Iterable<T>接口没有提供Stream和ParallelStream方法的原因。 - Brian Goetz
11个回答

698

一种方法是从Iterator创建一个Spliterator,然后将其用作流的基础:

Iterator<String> sourceIterator = Arrays.asList("A", "B", "C").iterator();
Stream<String> targetStream = StreamSupport.stream(
          Spliterators.spliteratorUnknownSize(sourceIterator, Spliterator.ORDERED),
          false);

另一种可能更易读的替代方法是使用 Iterable - 并且使用 lambda 从 Iterator 创建一个 Iterable 非常容易,因为 Iterable 是一个函数接口:

Iterator<String> sourceIterator = Arrays.asList("A", "B", "C").iterator();

Iterable<String> iterable = () -> sourceIterator;
Stream<String> targetStream = StreamSupport.stream(iterable.spliterator(), false);

34
Stream是惰性的:代码只是将Stream链接到迭代器,但实际迭代只有在终止操作时才会发生。如果您同时使用迭代器,您将无法获得预期的结果。例如,在使用流之前可以引入 sourceIterator.next() ,您将看到效果(流不会看到第一个项目)。 - assylias
12
@assylias,是的,它确实很好!也许你可以为未来的读者解释一下这个相当神奇的代码行“Iterable<String> iterable = () -> sourceIterator;”。我必须承认我花了一些时间才理解它。 - gontard
9
我应该说出我发现的事情。Iterable<T>是一个只有一个抽象方法iterator()FunctionalInterface。因此,() -> sourceIterator是一个lambda表达式,用作匿名实现来实例化一个Iterable实例。 - Jin Kwon
18
再次强调,() -> sourceIterator;new Iterable<>() { @Override public Iterator<String> iterator() { return sourceIterator; } } 的简写形式。在简写形式中,() 表示没有参数,箭头符号 -> 用于指示返回值,这里的返回值是 sourceIterator - Jin Kwon
10
@JinKwon,它并不是匿名类的简写形式(例如范围和编译方式存在一些微妙的差异),但在这种情况下它表现得相似。 - assylias
显示剩余16条评论

171

3
Javadoc文档链接:https://static.javadoc.io/com.google.guava/guava/21.0/com/google/common/collect/Streams.html#stream-java.util.Iterator-该链接为Google Guava库版本21.0中的Streams类中stream方法的Javadoc文档。该方法接受一个java.util.Iterator类型的参数,并返回一个流(Stream)。注意,Javadoc文档是Java程序员常用的API文档之一,用于了解类、方法或接口的详细信息,包括参数、返回值等。 - Henrik Aasted Sørensen
最好一直使用这种方法,直到JDK支持本地的一行代码。这样以后寻找(因此重构)会更简单,比在其他地方显示的纯JDK解决方案更容易找到。 - drekbour
8
这很棒,但是……Java如何拥有本地的迭代器和流,却没有内置、简单直接的方法可以在它们之间进行转换?在我看来,这是一个相当大的遗漏。 - Dan Lenski

109

好建议!以下是我对此的可重复利用意见:


public class StreamUtils {

    public static <T> Stream<T> asStream(Iterator<T> sourceIterator) {
        return asStream(sourceIterator, false);
    }

    public static <T> Stream<T> asStream(Iterator<T> sourceIterator, boolean parallel) {
        Iterable<T> iterable = () -> sourceIterator;
        return StreamSupport.stream(iterable.spliterator(), parallel);
    }
}

并使用(请确保静态导入asStream):

List<String> aPrefixedStrings = asStream(sourceIterator)
                .filter(t -> t.startsWith("A"))
                .collect(toList());

93

这在Java 9中是可能的。

Stream.generate(() -> null)
    .takeWhile(x -> iterator.hasNext())
    .map(n -> iterator.next())
    .forEach(System.out::println);

1
简单、高效且不需要使用子类化——这应该是被接受的答案! - martyglaubitz
3
不幸的是,它们似乎不能与.parallel()流一起使用。即使在顺序使用时,它们看起来比使用Spliterator要慢一些。 - Thomas Ahle
1
另外,如果迭代器为空,第一种方法会抛出异常。第二种方法目前可以工作,但它违反了 map 和 takeWhile 函数必须是无状态的要求,因此我不愿在生产代码中这样做。 - Hans-Peter Störr
实际上,这应该是一个被接受的答案。尽管“parallel”可能有些奇怪,但它的简单性是惊人的。 - Sven
5
你真的不应该使用副作用和变异。 - Adam Bickford
1
虽然在顺序流管道上,takeWhile()通常是一项廉价操作,但在有序并行管道上,它可能会非常昂贵。 参考:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/stream/Stream.html#takeWhile(java.util.function.Predicate) - dbaltor

11
import com.google.common.collect.Streams;

使用 Streams.stream(iterator)

Streams.stream(iterator)
       .map(v-> function(v))
       .collect(Collectors.toList());

11

使用Spliterators类从Iterator创建Spliterator有多个函数可用于创建分割器,例如这里我正在使用spliteratorUnknownSize,它将迭代器作为参数,然后使用StreamSupport创建流。

Spliterator<Model> spliterator = Spliterators.spliteratorUnknownSize(
        iterator, Spliterator.NONNULL);
Stream<Model> stream = StreamSupport.stream(spliterator, false);

1

编辑:不要这样做!

(仅为档案目的而保留)

另一种在Java 9+上执行此操作的方法是使用{{link1:Stream::iterate(T,Predicate,UnaryOperator)}}:

Stream.iterate(iterator, Iterator::hasNext, UnaryOperator.identity())
        .map(Iterator::next)
        .forEach(System.out::println);

7
mapforEach 中调用有副作用的代码是不安全的。 - Doradus

1

1assylias的解决方案包装在一个方法中:

public static <T> Stream<T> toStream(Iterator<T> iterator) {
    return StreamSupport.stream(((Iterable<T>)() -> iterator).spliterator(), false);
}

2 guava Streams 实现(标有@Beta):

public static <T> Stream<T> stream(Iterator<T> iterator) {
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
}

1

0
如果迭代大小已知,这是可能的:
public static void main(String[] args) throws Exception {
    Iterator<String> sourceIterator = Arrays.asList("A", "B", "C").iterator();
    Stream<String> targetStream = Stream.generate(sourceIterator::next);
    targetStream.**limit(3)**.forEach(System.out::println);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接