使用 LongStream 和 jOOλ 生成素数会导致 ***Error

Posted

技术标签:

【中文标题】使用 LongStream 和 jOOλ 生成素数会导致 ***Error【英文标题】:Generating primes with LongStream and jOOλ leads to ***Error 【发布时间】:2016-06-28 08:37:49 【问题描述】:

出于教育目的,我想使用 Java-8 创建一个素数流。这是我的方法。如果没有不超过sqrt(x) 的素数除数,则数字x 是素数。所以假设我已经有一个素数流,我可以用下面的谓词来检查:

x -> Seq.seq(primes()).limitWhile(p -> p <= Math.sqrt(x)).allMatch(p -> x % p != 0)

在这里,我使用 jOOλ 库(如果重要,则为 0.9.10)仅用于标准 Stream API 中不存在的 limitWhile 操作。所以现在知道了一些先前的素数 prev 我可以生成下一个素数迭代数字,直到找到与该谓词匹配的那个:

prev -> LongStream.iterate(prev + 1, i -> i + 1)
                  .filter(x -> Seq.seq(primes()).limitWhile(p -> p <= Math.sqrt(x))
                                                .allMatch(p -> x % p != 0))
                  .findFirst()
                  .getAsLong()

把所有东西放在一起我写了以下primes()方法:

public static LongStream primes() 
    return LongStream.iterate(2L, 
            prev -> LongStream.iterate(prev + 1, i -> i + 1)
                              .filter(x -> Seq.seq(primes())
                                              .limitWhile(p -> p <= Math.sqrt(x))
                                              .allMatch(p -> x % p != 0))
                              .findFirst()
                              .getAsLong());

现在启动这个我使用:

primes().forEach(System.out::println);

不幸的是,它以令人不快的***Error 失败,看起来像这样:

Exception in thread "main" java.lang.***Error
at java.util.stream.ReferencePipeline$StatelessOp.opIsStateful(ReferencePipeline.java:624)
at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:211)
at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618)
at java.util.stream.LongPipeline$3.<init>(LongPipeline.java:225)
at java.util.stream.LongPipeline.mapToObj(LongPipeline.java:224)
at java.util.stream.LongPipeline.boxed(LongPipeline.java:201)
at org.jooq.lambda.Seq.seq(Seq.java:2481)
at Primes.lambda$2(Primes.java:13)
at Primes$$Lambda$4/1555009629.test(Unknown Source)
at java.util.stream.LongPipeline$8$1.accept(LongPipeline.java:324)
at java.util.Spliterators$LongIteratorSpliterator.tryAdvance(Spliterators.java:2009)
at java.util.stream.LongPipeline.forEachWithCancel(LongPipeline.java:160)
at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:529)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:516)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.findFirst(LongPipeline.java:474)
at Primes.lambda$0(Primes.java:14)
at Primes$$Lambda$1/918221580.applyAsLong(Unknown Source)
at java.util.stream.LongStream$1.nextLong(LongStream.java:747)
at java.util.Spliterators$LongIteratorSpliterator.tryAdvance(Spliterators.java:2009)
...

您可能认为我应得的:我在 primes() 方法本身内部递归地调用了 primes()。但是,我们只需将方法返回类型更改为 Stream&lt;Long&gt; 并改用 Stream.iterate,其他一切都保持原样:

public static Stream<Long> primes() 
    return Stream.iterate(2L, 
            prev -> LongStream.iterate(prev + 1, i -> i + 1)
                              .filter(x -> Seq.seq(primes())
                                              .limitWhile(p -> p <= Math.sqrt(x))
                                              .allMatch(p -> x % p != 0))
                              .findFirst()
                              .getAsLong());

现在它就像一个魅力!不是很快,但在几分钟内我得到超过 1000000 的素数,没有任何例外。结果是正确的,可以对照素数表检查:

System.out.println(primes().skip(9999).findFirst());
// prints Optional[104729] which is actually 10000th prime.

所以问题是:第一个基于LongStream 的版本有什么问题?是 jOOλ 错误、JDK 错误还是我做错了什么?

请注意,我对生成素数的替代方法不感兴趣,我想知道这个特定代码有什么问题。

【问题讨论】:

这不是JOOL。用等效的x -&gt; primes().filter(p -&gt; p * p &gt; x || x % p == 0).findFirst().get() &gt; Math.sqrt(x) 替换基于 Seq 的过滤器具有相同的行为。适用于Stream&lt;Long&gt;,但不适用于LongStream 【参考方案1】:

iterate 生成流时,LongStreamStream 的行为似乎不同。下面的代码说明了区别:

LongStream.iterate(1, i -> 
    System.out.println("LongStream incrementing " + i);
    return i + 1;
).limit(1).count();

Stream.iterate(1L, i -> 
    System.out.println("Stream incrementing " + i);
    return i + 1;
).limit(1).count();

输出是

LongStream 递增 1

所以LongStream 将调用该函数,即使只需要第一个元素,而Stream 则不需要。这解释了您遇到的异常。

我不知道这是否应该称为错误。 Javadoc 没有以一种或另一种方式指定这种行为,但如果它是一致的就好了。

修复它的一种方法是硬编码初始素数序列:

public static LongStream primes() 
    return LongStream.iterate(2L,
        prev -> prev == 2 ? 3 : 
                prev == 3 ? 5 :
                LongStream.iterate(prev + 1, i -> i + 1)
                        .filter(x -> Seq.seq(primes())
                            .limitWhile(p -> p <= Math.sqrt(x))
                            .allMatch(p -> x % p != 0)
                        ).findFirst()
                        .getAsLong());

【讨论】:

【参考方案2】:

您可以通过更简单的方式产生这种差异。考虑以下两个版本的(同样低效的)递归长枚举流,可以按如下方式调用它以生成 1-5 的序列:

longs().limit(5).forEach(System.out::println);

会导致同样的 ***Error

public static LongStream longs() 
    return LongStream.iterate(1L, i ->
        1L + longs().skip(i - 1L)
                    .findFirst()
                    .getAsLong());

会工作

public static Stream<Long> longs() 
    return Stream.iterate(1L, i ->
        1L + longs().skip(i - 1L)
                    .findFirst()
                    .get());

原因

盒装的Stream.iterate()实现优化如下:

    final Iterator<T> iterator = new Iterator<T>() 
        @SuppressWarnings("unchecked")
        T t = (T) Streams.NONE;

        @Override
        public boolean hasNext() 
            return true;
        

        @Override
        public T next() 
            return t = (t == Streams.NONE) ? seed : f.apply(t);
        
    ;

不同于LongStream.iterate() 版本:

    final PrimitiveIterator.OfLong iterator = new PrimitiveIterator.OfLong() 
        long t = seed;

        @Override
        public boolean hasNext() 
            return true;
        

        @Override
        public long nextLong() 
            long v = t;
            t = f.applyAsLong(t);
            return v;
        
    ;

注意盒装迭代器如何仅在返回种子后调用函数,而原始迭代器在返回种子之前缓存下一个值。

这意味着当您使用带有原始迭代器的递归迭代函数时,永远无法生成流中的第一个值,因为过早地获取下一个值。

这可能被报告为 JDK 错误,也可能被报告为 explains Misha's observation

【讨论】:

感谢您出色的分析和揭示此问题的替代方法。无需报告错误:在 Java-9 中,我已经修复了它;-) @TagirValeev:很高兴知道。你有链接吗?您知道该修复程序是否会向后移植到 Java 8? @TagirValeev:没关系。 Found it again 我很确定它不会被移植。

以上是关于使用 LongStream 和 jOOλ 生成素数会导致 ***Error的主要内容,如果未能解决你的问题,请参考以下文章

使用 CUDA 生成素数时遇到问题

如何用VC++随机生成一个大素数(满足RSA算法)

如何生成前 n 个素数?

在m和n之间生成素数[重复]

数学符号λ的用法

当我想使用 calcHist 生成图片特定范围内的像素数时,断言失败