在Java中什么时候应该使用IntStream.range?

54

我想知道何时可以有效使用IntStream.range。 我有三个原因,为什么我不确定IntStream.range有多有用。

(请将起始和结束视为整数。)

  1. 如果我想要一个数组[start,start + 1,...,end-2,end-1],下面的代码速度更快。

int[] arr = new int[end - start];
int index = 0;
for(int i = start; i < end; i++)
    arr[index++] = i;

这可能是因为IntStream.range(start, end).toArray()中的toArray()非常慢。

  • 我使用MersenneTwister对数组进行洗牌。(我从网上下载了MersenneTwister类。)我不认为有办法使用MersenneTwister来洗牌IntStream

  • 我认为仅仅获取从startend-1int数字没有用处。我可以使用for(int i = start; i < end; i++),这似乎更容易且不会变慢。

  • 你能告诉我何时应该选择IntStream.range吗?


    4
    你可以使用IntStream.range()生成一个流,并将其作为参数传递给另一个方法。而用for语句做不到这一点。 - biziclop
    1
    面试题最好通过IntStream.range来解决:Array list algorithm - Interview - Joop Eggen
    2
    这可能是因为toArray()方法非常慢。您是如何衡量它的速度的?您能否发布或链接到一个有效的基准测试?并且"非常慢"是指什么? - Tunaki
    4
    很可能你的基准测试存在缺陷,衡量Java性能不像比较两个时间戳那么简单。 - Tagir Valeev
    2
    @Nickel:你的range2方法什么也没做。当然,将值写入数组需要比什么都不做更多的时间,但这与你声称toArray比执行相同操作的for循环慢有何关系? - Holger
    显示剩余4条评论
    7个回答

    47

    使用IntStream.range有几种方法。

    其中之一是使用int值本身:

    IntStream.range(start, end).filter(i -> isPrime(i))....
    

    另一种方法是重复N次某件事:

    IntStream.range(0, N).forEach(this::doSomething);
    

    你的情况(1)是要创建一个填充有一定范围的数组:

    int[] arr = IntStream.range(start, end).toArray();
    

    你说这个过程非常缓慢,但是和其他回答者一样,我怀疑你的基准测试方法。对于小数组来说,确实有更多的流设置开销,但这应该非常小到无法察觉。对于大数组来说,开销应该可以忽略,因为填充一个大数组受内存带宽的影响。

    有时你需要填充一个已存在的数组,你可以这样做:

    int[] arr = new int[end - start];
    IntStream.range(0, end - start).forEach(i -> arr[i] = i + start);
    

    有一个实用方法 Arrays.setAll ,可以更加简洁地完成这个任务:

    int[] arr = new int[end - start];
    Arrays.setAll(arr, i -> i + start);
    

    还有一个 Arrays.parallelSetAll 方法可以并行地填充一个已存在的数组。内部实现是使用了 IntStream 并在其上调用了 parallel() 方法。这样可以在多核系统上为大型数组提供加速。

    我发现我的很多 Stack Overflow 回答都涉及到使用 IntStream.range 方法。你可以在搜索框中使用以下搜索条件进行查找:

    user:1441122 IntStream.range
    

    我发现IntStream.range的一个特别有用的应用是在数组元素上进行操作,其中数组索引以及数组的值都参与计算。有一整类这样的问题。

    例如,假设你想要找到数组中递增数字的位置。结果是一个指向第一个数组中每个递增序列开头的索引数组。

    为了计算这个问题,观察到一个递增序列从前一个数值小于当前数值的位置开始。(一个递增序列也可以从位置0开始)。因此:

        int[] arr = { 1, 3, 5, 7, 9, 2, 4, 6, 3, 5, 0 };
        int[] runs = IntStream.range(0, arr.length)
                              .filter(i -> i == 0 || arr[i-1] > arr[i])
                              .toArray();
        System.out.println(Arrays.toString(runs));
    
        [0, 5, 8, 10]
    

    当然,你可以用for循环做到这一点,但是我发现在许多情况下使用IntStream更可取。例如,使用toArray()轻松地将未知数量的结果存储到数组中,而使用for循环则必须处理复制和调整大小,这会分散循环的核心逻辑。

    最后,使用IntStream.range计算更容易并行运行。


    3
    为什么在第三个示例中您使用forEach来写入预分配的数组,而不是使用干净的int [] arr = IntStream.range(start,end).toArray(); - Holger
    2
    @Holger 有时候你想要填充一个已有的数组。OP 声称 IntStream.range(start, end).toArray() 太慢了,所以他显然知道这一点。但是我还是应该澄清一下上下文。此外,只有 Arrays.parallelSetAll 使用了 IntStream,因此我也会进行调整。 - Stuart Marks
    @Holger 谢谢您的建议。很抱歉,我当时非常困惑,在我的基准测试中犯了错误。然而,事实是,在我的笔记本电脑上(Core i7 4710MQ,Java8u92),使用预先制作的数组比使用toArray()更快。 - user5790923
    int[] arr = new int[end - start]; // Arrays.setAll(i -> i + start); 看起来不对 - Arrays.setAll() 缺少一个参数? - MyStackRunnethOver
    2
    @MyStackRunnethOver 嗯,缺少参数,谢谢。已修复。 - Stuart Marks

    7

    IntStream.range返回一个整数范围流,以便您可以对其进行流处理。

    例如,对每个元素取平方。

    IntStream.range(1, 10).map(i -> i * i);  
    

    1
    类似于 C# 的 Enumerable.Range https://msdn.microsoft.com/zh-cn/library/system.linq.enumerable.range(v=vs.110).aspx - JonH

    7

    这里有一个例子:

    public class Test {
    
        public static void main(String[] args) {
            System.out.println(sum(LongStream.of(40,2))); // call A
            System.out.println(sum(LongStream.range(1,100_000_000))); //call B
        }
    
        public static long sum(LongStream in) {
            return in.sum();
        }
    
    }
    

    因此,让我们看一下sum()的作用:它计算任意流数字的总和。我们以两种不同的方式调用它:一次使用显式数字列表,一次使用范围。
    如果你只有call A,你可能会想把这两个数字放入数组中并将其传递给sum(),但对于call B来说,这显然不是一个选择(你会耗尽内存)。同样,你可以直接传递call B的开始和结束,但那样你就无法支持call A的情况。
    因此,范围在这里非常有用,因为:
    • 我们需要在方法之间传递它们
    • 目标方法不仅适用于范围,还适用于任何数字流
    • 但它仅对流的单个数字进行操作,按顺序读取它们。(这就是为什么通常使用流洗牌是一个可怕的主意)
    还有可读性的论点:使用流的代码可以比循环更简洁,因此更易读,但我想展示一个依赖于IntStrean的解决方案在功能上也更加优越的例子。 我使用LongStream来强调这一点,但对于IntStream也是如此。 是的,对于简单的求和,这可能看起来有点过度,但考虑例如蓄水池抽样

    1
    说到可读性,我宁愿写100_000_000,但那只是我的想法 :-) - Jean-François Savard
    3
    好的,我只是随意地输入了很多零,没有数清楚 :) - biziclop
    谢谢!蓄水池抽样是一个很好的例子! - user5790923

    3
    以下是我想到的IntStream.range和传统for循环之间的几点不同之处:
    • IntStream会延迟求值,当调用terminal操作时才遍历流水线。而for循环在每次迭代时都会求值。
    • IntStream提供了一些常用于int范围的函数,例如sumavg
    • IntStream允许您以函数式方式编写多个针对int范围的操作,这种方式更加流畅易读——特别是在有大量操作时。

    因此,当这些不同之处中的一个或多个对您有用时,请使用IntStream

    但请记住,打乱Stream听起来相当奇怪,因为Stream不是数据结构,因此将其打乱并没有真正意义(如果您计划构建特殊的IntSupplier)。而是打乱结果。

    至于性能,虽然可能存在一些开销,在两种情况下您仍将迭代N次,并且不应过于关注它。


    2

    基本上,如果你想要使用Stream操作,你可以使用range()方法。例如,如果你想要并发或者想要使用map()或者reduce(),那么你最好使用IntStream

    例如:

    IntStream.range(1, 5).parallel().forEach(i -> heavyOperation());
    

    或者:

    IntStream.range(1, 5).reduce(1, (x, y) -> x * y)  
    // > 24
    

    你也可以使用for循环来实现第二个示例,但是你需要中间变量等。

    此外,如果你想要找到第一个匹配项,可以使用findFirst()和相关方法停止消耗Stream的其余部分。


    在我看来,“better”这个词并不是很合适。 - Jean-François Savard
    @Jean-FrançoisSavard,请解释一下你的观点? - Rob Audenaerde
    第二个等同于 for(int i = 1; i < 5; i++) result *= i;。两者不同,但都不是更好的。中间变量并不是真正的问题(如果你只是做乘法,使用流方法会有很多开销)。第一个似乎缺少一点解释。当然,并行可以是一个好的方法,但在你的例子中,我宁愿创建一个线程池,其中包含5个线程来执行heavyOperation,实际上,在这个例子中根本没有使用i。 - Jean-François Savard
    2
    为了明确 - 我并不争辩传统循环是更好还是更差。两者都可以,但如果我知道我的循环可能需要执行大量操作,那么我宁愿使用 IntStream,这样我的代码会更流畅(以一种函数式的方式)。 - Jean-François Savard

    2

    这完全取决于具体的应用场景。然而,语法和流API添加了很多易于理解的一行代码,可以完全替代传统的循环。

    IntStream在某些情况下确实非常有用且语法简洁。

    IntStream.range(1, 101).sum();
    IntStream.range(1, 101).average();
    IntStream.range(1, 101).filter(i -> i % 2 == 0).count();
    //... and so on
    

    您可以使用传统循环完成IntStream所能完成的所有操作。一行代码更加精确易懂并且易于维护。

    但是对于负循环,我们不能使用IntStream#range,它仅适用于正增量。因此,以下操作不可能实现:

    for(int i = 100; i > 1; i--) {
        // Negative loop
    }
    
    • 案例1:在这种情况下,传统的循环要快得多,因为toArray有一点额外开销。

    • 案例2:对此我一无所知,请谅解。

    • 案例3:IntStream并不慢,IntStream.range和传统循环在性能方面几乎是相同的

    请参见:


    1
    或者,你可以使用 IntStream.iterate(from - 1, i -> i - 1).limit(from - to) 这种方式来实现。 - Jean-François Savard
    1
    IntStream和传统的for循环一样快(除非调用parallel()方法),但它更节省内存,并且需要更少的代码。这是正确的吗? - user5790923
    3
    对于大多数用例,顺序的 IntStream 与传统循环速度相当,尽管这取决于许多因素。最好说数量级是相同的,轻微且不可预测的差异是无关紧要的。它往往需要更多的内存,但这是一种临时的内存使用,甚至可能不被注意到。最重要的区别在于,使用 IntStream 可以用更简单的代码表达许多任务。 - Holger
    2
    @Jean-François Savard:不要低估HotSpot优化器。一个应用在int上的%2很容易识别。由于它的实现方式,range(…).filter(…)可能与.iterate(…).limit(…)一样甚至更高效。 - Holger

    0

    你可以将你的 Mersenne Twister 实现为一个 Iterator,并从中生成流


    1
    我也有类似的想法,但是实现方式是将MT作为IntSupplier,然后使用IntStream#generate - bradimus

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