流和惰性求值

68
我正在阅读Java 8 API中有关流抽象的内容,但是我不太理解以下这句话:
中间操作返回一个新的流。它们总是惰性的;执行诸如filter()之类的中间操作并不实际执行任何过滤,而是创建一个新的流,该流在遍历时包含与给定谓词匹配的初始流的元素。 对于管道源的遍历直到管道的终端操作执行时才开始。
当filter操作创建一个新流时,该流是否包含经过过滤的元素?看起来只有在遍历流时即执行终端操作时,流才包含元素。但是,那么这个经过过滤的流包含什么呢?我很困惑!

1
@Lukas,我们已经有了 [tag:java-stream] 用于 Java 8 Stream API。你创建的标签太过泛化。 - Charles
16
它的官方名称是“Streams API”,而不是“Java Stream”。Java Stream 可以指任何东西,包括 InputStream / OutputStream 类型等。无论如何,我猜这应该被带到元数据中... - Lukas Eder
3个回答

84
这意味着过滤器仅在终端操作期间应用。可以将其想象为以下情况:
public Stream filter(Predicate p) {
    this.filter = p; // just store it, don't apply it yet
    return this; // in reality: return a new stream
}
public List collect() {
    for (Object o : stream) {
        if (filter.test(o)) list.add(o);
    }
    return list;
}

(这段代码无法编译,只是对实际情况的简化,但原理存在)

37

流式处理是一种延迟计算的方式,只有在触发终端操作时才对中间操作进行求值。

每个中间操作都会创建一个新的流,保存提供的操作/函数,并返回新的流。

管道会累积这些新创建的流。

当调用终端操作时,开始遍历流并逐个执行相关函数。

并行流不会按“逐个”顺序(在终点处)对流进行评估。相反,操作会同时执行,具体取决于可用的内核。


0

在我看来,中间操作并不完全是惰性的:

List<String> l3 = new ArrayList<String>();
        l3.add("first");
        l3.add("second");
        l3.add("third");
        l3.add("fouth");
        l3.add("fith");
        l3.add("sixth");

        List<String> test3 = new ArrayList<String>();
        try {
            l3.stream().filter(s -> { l3.clear(); test3.add(s); return true;}).forEach(System.out::println);
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("!!! ");
            System.out.println(test3.stream().reduce((s1, s2) -> s1 += " ;" + s2).get());
        }

输出:

  first
    null
    null
    null
    null
    null
    java.util.ConcurrentModificationException
        at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1380)
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
        at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
        at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
        at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
        at test.TestParallel.main(TestParallel.java:69)
    !!! 

    first ;null ;null ;null ;null ;null

看起来是在流创建时设置迭代次数,但是获取新的流元素是惰性的。

与计数器循环相比:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(); 
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    list.add(5);
    int i = 0;
    while (i < list.size()) {
        System.out.println(list.get(i++));
        list.clear();
    }
}

输出:

1

只有一个预期迭代。

我同意流中异常抛出行为的问题,但我认为“惰性”意味着只有在我要求某个对象执行操作或获取数据时才会执行;而数据的计数也是数据。


请您将代码格式化,方法是将其高亮显示,然后按Ctrl+K。 - WhatsThePoint
1
仅仅因为它迭代了6次并不意味着它不是惰性的。看看ArrayList第1380行(forEachRemaining)的代码,发生的情况是直到整个列表都被迭代后才会抛出ConcurrentModificationExcception异常。forEachRemaining是一个“终端操作”。 - Nicole
1
直到迭代整个列表后,它才会抛出ConcurrentModificationExcception异常。我认为只有在迭代“整个列表”(在list.clear()之后 - 整个列表为空列表)之后才抛出ConcurrentModificationExcception异常不是一种懒惰的行为。如果我使用迭代器执行相同的操作,那么在抛出ConcurrentModificationExcception异常之前,我只能看到一个输出; - Андрей Палкин
好的,这个例子与Java Streams中的惰性关系不大。如果一个中间操作不是惰性的,仅调用filter就会遍历元素。但实际上并没有这样,所以流的中间操作确实是惰性的。 - MC Emperor
我找到了这个定义 - “惰性求值意味着推迟计算,直到需要结果” 链接。 在这个例子中 - 当需要结果时 - 需要2个元素,但我只得到了1个: int[] a = {2}; IntStream.range(0, 10).limit(--a[0]).peek(System.out::println).limit(++a[0]).forEach(i -> {});你认为“Java Stream上下文中的惰性”有什么意义? - Андрей Палкин

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