Java 8流 - stackoverflow异常

12

运行以下代码示例会以以下方式结束:
“Exception in thread "main" java.lang.StackOverflowError”

import java.util.stream.IntStream;
import java.util.stream.Stream;

public class TestStream {

    public static void main(String[] args) {
        Stream<String> reducedStream = IntStream.range(0, 15000)
            .mapToObj(Abc::new)
            .reduce(
                Stream.of("Test")
                , (str , abc) -> abc.process(str)
                , (a , b) -> {throw new IllegalStateException();}
        );
        System.out.println(reducedStream.findFirst().get());
    }

    private static class Abc { 
        public Abc(int id) {
        }

        public Stream<String> process(Stream<String> batch) {
            return batch.map(this::doNothing);
        }

        private String doNothing(String test) {
            return test;
        }
    }
}

是什么导致了这个问题?这段代码的哪一部分是递归的,为什么会出现递归?


4
为什么要将一个流(Stream)缩减为另一个流(Stream)? - Tunaki
1
问题出在对 map 方法的 15000 次调用上。在内部,它必须以某种方式进行链式调用。您可以使用以下代码重现此问题,而无需使用 AbcIntStream.range(0, 15000).boxed().reduce(Stream.of("Test"), (str , abc) -> str.map(s -> s), (a , b) -> {throw new IllegalStateException();} - Tunaki
4
StackOverflowError 的堆栈跟踪应该揭示方法相互调用的循环。请在您的帖子中包含相关部分。 StackOverflowError 的堆栈跟踪应该显示相互调用的方法循环。请在您的文章中引用相关部分。 - rgettman
2
更简短的代码:Stream<String> st = Stream.of("Test"); for (int i = 0; i < 15000; i++) st = st.map(s -> s); st.findFirst(); 我不确定我们是否可以将其归类为一个 bug。每个 map 操作都会创建一个临时对象来包装当前的 Stream。当遍历开始时,必须经过所有这些间接操作。而且似乎太多了。 - Tunaki
3
注意:这个错误也在这张工单中提到:https://bugs.openjdk.java.net/browse/JDK-8025523,由Paul Sandoz撰写。请注意,这并不能阻止所有堆栈溢出,还有可能通过其他方式创建堆栈溢出,例如创建非常长的管道,例如使用许多map(Function.identity())操作的Stream.of(1),并使用像toArray()这样的终端操作。在这方面,在文档中加入一些实施注释可能是合适的。 - Tunaki
显示剩余3条评论
1个回答

4

你的代码没有递归循环。你可以用较小的数字测试IntStream范围(例如1或100)。在你的情况下,实际堆栈大小限制导致了问题。正如一些评论中指出的那样,它是处理流的方式引起的。

流上的每次调用都会在原始流周围创建一个新的包装流。'findFirst()'方法要求前一个流的元素,这转而要求前一个流的元素。由于流不是真正的容器而只是指向结果元素的指针。

包装器爆炸发生在reduce方法的累加器'(str, abc) -> abc.process(str)'中。该方法的实现在上一个操作的结果(str)上创建一个新的流包装器,并进入下一次迭代,在结果(result(str))上创建新的包装器。因此,累积机制是包装器(递归)而不是追加器(迭代)。因此,创建实际(铺平的)结果的新流而不是参考潜在结果的新流将停止扩展。

public Stream<String> process(Stream<String> batch) {
        return Stream.of(batch.map(this::doNothing).collect(Collectors.joining()));
    }

这个方法只是一个例子,因为你原本的例子没有任何意义,以及这个例子也一样。它只是一个说明。它基本上将映射方法返回的流的元素展开成一个单独的字符串,并在该具体字符串上创建一个新的流,而不是在流本身上创建一个新的流,这就是与你原来的代码的区别。

您可以使用“-Xss”参数调整堆栈大小,该参数定义每个线程的堆栈大小。默认值取决于平台,请参见此问题“What is the maximum depth of the java call stack?” 但是要注意,在增加时,此设置适用于所有线程。


谢谢。我需要花点时间来调试Java流。 这个是有效的代码: return Stream.of(batch.map(this::doNothing).collect(Collectors.joining())); 但这一行代码非常奇怪 :) - slowikps

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