使用Java 8求整数列表的总和

5

我只是在尝试使用Java 8,通过编写一个程序来计算大型列表中偶数的总和,并与Java 6进行比较。

Java 8

public class ListOperationsJava8 {

    static List<BigInteger> list = new LinkedList<>();

    public static void main(String[] args) {
        createList();

        long start = System.currentTimeMillis();

        /*System.out.println(list.parallelStream().
                filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO)).
                        mapToInt(BigInteger::intValue).sum());  --> gives result        -1795017296 */

        System.out.println(list.parallelStream().
                filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO)).
                    mapToLong(BigInteger::longValue).sum());

        long end = System.currentTimeMillis();

        System.out.println("Time taken using Java 8:  " + (end - start) + " ms");
    }

    private static void createList() {
        for (int i = 0; i < 100000; i++) {
            list.add(new BigInteger(String.valueOf(i)));
        }
    }
}

Java 6

public class ListOperationsClassic {

    static List<BigInteger> list = new LinkedList<BigInteger>();

    public static void main(String[] args) {
        createList();

        long start = System.currentTimeMillis();

        BigInteger sum = BigInteger.ZERO;

        for(BigInteger n : list) {
            if(n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
                sum = sum.add(n);
        }

        System.out.println(sum);

        long end = System.currentTimeMillis();

        System.out.println("Time taken using Java 6: " + (end - start) + " ms");
    }

    private static void createList() {
        for (int i = 0; i < 100000; i++) {
            list.add(new BigInteger(String.valueOf(i)));
        }
    }
}

我有两个问题

  1. 在Java 8代码中,我最初使用了mapToInt(BigInteger::intValue).sum())来减少值,但是得到了负数结果-1795017296!为什么?我理解期望的结果2499950000本身在int可以表示的范围内。
  2. 我运行了几次代码,我总是发现Java 8的代码比使用Java 6的代码要慢多达5倍。这可能意味着什么?对于这种规模的操作,并没有必要使用parallelStream和/或reduce操作,普通的for循环更好吗?

这是其中一个结果:

2499950000
Time taken using Java 6: 52 ms

2499950000
Time taken using Java 8:  249 ms

4
“根据我的理解,总值2499950000本身在可以用int表示的范围内。” 不,int的最大值是2147483647,比这个小。 - Alexis C.
尝试在不调用 System.out.println(list.parallelStream()... 的情况下运行它。这会大大增加您的执行时间。 - Bucket
1
并行流具有足够的开销,仅在较长的迭代时间(每个元素的持续时间)中才值得使用,并且可以并行执行。如果您调用了 Thread.sleep(100),则统计信息将恢复为原始状态。 - Joop Eggen
@DesertIvy 在这两种情况下都有 System.out.println,所以它不可能有影响。对于 lambda 表达式也没有影响,因为打印肯定是最后执行的事情。 - skiwi
1
@zencv 最大值为2^31-1,而不是2^32-1 - Alexis C.
显示剩余2条评论
3个回答

6

这是我的快速而简略的基准测试,允许在每个测试之间进行JIT热身和GC。结果如下:

  • for循环:686毫秒
  • lambda:681毫秒
  • 并行lambda:405毫秒

请注意,我已经修改了您的代码,尽可能使三个测试相等 - 特别是对于lambdas,我使用缩减将BigIntegers添加在一起,而不是转换为long。
我还删除了每次迭代中不必要的 new BigInteger("2") 创建。

public class Test1 {

    static List<BigInteger> list = new LinkedList<>();
    static BigInteger TWO = new BigInteger("2");

    public static void main(String[] args) {
        createList();

        long sum = 0;

        //warm-up
        for (int i = 0; i < 100; i++) {
            sum += forLoop().longValue();
            sum += lambda().longValue();
            sum += parallelLambda().longValue();
        }

        {
            System.gc();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) sum += forLoop().longValue();
            long end = System.currentTimeMillis();
            System.out.println("Time taken using for loop:  " + (end - start) + " ms");
        }

        {
            System.gc();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) sum += lambda().longValue();
            long end = System.currentTimeMillis();
            System.out.println("Time taken using lambda:  " + (end - start) + " ms");
        }

        {
            System.gc();
            long start = System.currentTimeMillis();
            for (int i = 0; i < 100; i++) sum += parallelLambda().longValue();
            long end = System.currentTimeMillis();
            System.out.println("Time taken using parallelLambda:  " + (end - start) + " ms");
        }
    }

    private static void createList() {
        for (int i = 0; i < 100000; i++) {
            list.add(new BigInteger(String.valueOf(i)));
        }
    }

    private static BigInteger forLoop() {
        BigInteger sum = BigInteger.ZERO;

        for(BigInteger n : list) {
            if(n.mod(TWO).equals(BigInteger.ZERO))
                sum = sum.add(n);
        }
        return sum;
    }

    private static BigInteger lambda() {
        return list.stream().
                filter(n -> n.mod(TWO).equals(ZERO)).
                reduce(ZERO, BigInteger::add);
    }

    private static BigInteger parallelLambda() {
        return list.parallelStream().
                filter(n -> n.mod(TWO).equals(ZERO)).
                reduce(ZERO, BigInteger::add);
    }
}

谢谢(+1),我会测试一下。据我所知,您在lambda和for循环版本中都使用了Jdk 8? - senseiwu
是的,对于所有测试都使用jdk8 - 但我不认为它会有显著的差异。 - assylias

5
针对你的第一个问题:你的值可能大于Integer.MAX_VALUE,从而导致溢出,这意味着它将被外部表示为从Integer.MIN_VALUE开始。关键字在于溢出
至于第二个问题,我不知道为什么会有差异,可能最终可以在字节码中找到答案,但这真的是一个问题吗?还是一个过早优化的情况?如果是前者,那么你应该担心,如果是后者,那么你不应该关注它是否更慢。
我观察到你的代码有一个区别:
  • Java 6:你使用一个BigInteger sum,并在其上调用.add()方法。
  • Java 8:你使用一些long变量,并在内部将其相加。
此外,你在进行非常快速的计算时使用了parallelStream(),这可能会导致创建新的底层线程所需的时间比使用普通线性stream()更长。
此外,如果你真的在测量速度,那么你应该进行更多的测试,每个测试用例运行多次,因为计时可能取决于其他因素 - JVM、CPU的时钟速度等等。
最后一次编辑,你的Java 8代码实际上并不代表你的Java 6代码,应该是:
final BigInteger sum = BigInteger.ZERO;
list.stream()
.filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
.forEach(n -> { sum = sum.add(n); });

为了充分展示您的Java 6代码,需要注意引入“sum”并不是一个好的选择,并且如果用于并行计算,这段代码根本无法工作。
最后,为了正确展示在Java 8中应该如何做,看起来更正确,适用于并行版本,甚至可以在线性版本中提高性能:
    Optional<BigInteger> sum = list.stream()
            .filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
            .reduce((n1, n2) -> n1.add(n2));
    System.out.println(sum.get());

在这里,我使用了reduce()运算符,它以BinaryOperator<T>作为参数,我用它来将BigInteger相加。
一个注意点是,它可能会遇到流为空的情况,所以它不知道是否有值,因此返回一个Optional<T>
请仔细注意,通常情况下您需要调用sum.isPresent()检查它是否实际上有一个值,但在这种情况下,我们知道必须有一个值,因此直接调用sum.get()
最后,我已经在我的个人电脑上测试了1百万个数字的不同版本:
  • Java 6: 大约190〜210毫秒。
  • Java 8你的代码:大约160〜220毫秒。
  • Java 8,我的线性代码:大约180〜260毫秒。
  • Java 8,我的并行代码:大约180〜270毫秒。
因此,由于这不是真正的微基准测试,我认为结论是,在这种情况下使用什么并不重要。

谢谢你展示 Optional 和 reduce,我一定会尝试它们的。 - senseiwu

2

您可以直接调用reduce方法将数字添加到BigDecimal中,而无需将其转换为long。

list.parallelStream()
.filter(n -> n.mod(new BigInteger("2")).equals(BigInteger.ZERO))
.reduce(BigDecimal.ZERO, BigDecimal::add)

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