昨天写了一篇文章《小细节,大问题。共享一次代码优化的进程》,里面提到了list.sort()和list.strem().sorted()排序的差异。
提到list sort()排序比stream().sorted()排序功能更好。
但没提到为什么。

为什么list.sort()比Stream().sorted()更快?

有朋友也提到了这一点。

本文重新开始,先问是不是,再问为什么。


真的更好吗?


先简略写个demo

List<Integer> userList = new ArrayList<>();
        Random rand = new Random();
        for (int i = 0; i < 10000 ; i++) {
            userList.add(rand.nextInt(1000));
        }
        List<Integer> userList2 = new ArrayList<>();
        userList2.addAll(userList);
        Long startTime1 = System.currentTimeMillis();
        userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
        System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");
        Long startTime = System.currentTimeMillis();
        userList.sort(Comparator.comparing(Integer::intValue));
        System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");

输出

stream.sort耗时:62ms
List.sort()耗时:7ms

由此可见list原生排序功能更好。
能证明吗?
依据错了。


再把demo改换一下,先输出stream.sort

List<Integer> userList = new ArrayList<>();
        Random rand = new Random();
        for (int i = 0; i < 10000 ; i++) {
            userList.add(rand.nextInt(1000));
        }
        List<Integer> userList2 = new ArrayList<>();
        userList2.addAll(userList);
        Long startTime = System.currentTimeMillis();
        userList.sort(Comparator.comparing(Integer::intValue));
        System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");
        Long startTime1 = System.currentTimeMillis();
        userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
        System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");

此刻输出变成了

List.sort()耗时:68ms
stream.sort耗时:13ms

这能证明上面的结论错误了吗?
都不能。
两种办法都不能证明什么。

运用这种办法在很多场景下是不够的,某些场景下,JVM会对代码进行JIT编译和内联优化。

Long startTime = System.currentTimeMillis();
...
System.currentTimeMillis() - startTime

此刻,代码优化前后履行的成果就会非常大。

基准测验是指经过规划科学的测验办法、测验东西和测验系统,完成对一类测验对象的某项功能指标进行定量的和可对比的测验。

基准测验使得被测验代码获得足够预热,让被测验代码得到充沛的JIT编译和优化。


下面是经过JMH做一下基准测验,分别测验调集大小在100,10000,100000时两种排序办法的功能差异。

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 5)
@Fork(1)
@State(Scope.Thread)
public class SortBenchmark {
    @Param(value = {"100", "10000", "100000"})
    private int operationSize; 
    private static List<Integer> arrayList;
    public static void main(String[] args) throws RunnerException {
        // 发动基准测验
        Options opt = new OptionsBuilder()
                .include(SortBenchmark.class.getSimpleName()) 
                .result("SortBenchmark.json")
                .mode(Mode.All)
                .resultFormat(ResultFormatType.JSON)
                .build();
        new Runner(opt).run(); 
    }
    @Setup
    public void init() {
        arrayList = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < operationSize; i++) {
            arrayList.add(random.nextInt(10000));
        }
    }
    @Benchmark
    public void sort(Blackhole blackhole) {
        arrayList.sort(Comparator.comparing(e -> e));
        blackhole.consume(arrayList);
    }
    @Benchmark
    public void streamSorted(Blackhole blackhole) {
        arrayList = arrayList.stream().sorted(Comparator.comparing(e -> e)).collect(Collectors.toList());
        blackhole.consume(arrayList);
    }
}

功能测验成果:

为什么list.sort()比Stream().sorted()更快?

能够看到,list sort()功率的确比stream().sorted()要好。


为什么更好?


流本身的损耗


java的stream让咱们能够在应用层就能够高效地完成相似数据库SQL的聚合操作了,它能够让代码愈加简洁优雅。

但是,假设咱们要对一个list排序,得先把list转成stream流,排序完成后需要将数据搜集起来重新构成list,这部份额外的开支有多大呢?

咱们能够经过以下代码来进行基准测验

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 5)
@Fork(1)
@State(Scope.Thread)
public class SortBenchmark3 {
    @Param(value = {"100", "10000"})
    private int operationSize; // 操作次数
    private static List<Integer> arrayList;
    public static void main(String[] args) throws RunnerException {
        // 发动基准测验
        Options opt = new OptionsBuilder()
                .include(SortBenchmark3.class.getSimpleName()) // 要导入的测验类
                .result("SortBenchmark3.json")
                .mode(Mode.All)
                .resultFormat(ResultFormatType.JSON)
                .build();
        new Runner(opt).run(); // 履行测验
    }
    @Setup
    public void init() {
        // 发动履行事件
        arrayList = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < operationSize; i++) {
            arrayList.add(random.nextInt(10000));
        }
    }
    @Benchmark
    public void stream(Blackhole blackhole) {
        arrayList.stream().collect(Collectors.toList());
        blackhole.consume(arrayList);
    }
    @Benchmark
    public void sort(Blackhole blackhole) {
        arrayList.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
        blackhole.consume(arrayList);
    }
}

办法stream测验将一个调集转为流再搜集回来的耗时。

办法sort测验将一个调集转为流再排序再搜集回来的全进程耗时。


测验成果如下:

为什么list.sort()比Stream().sorted()更快?

能够发现,调集转为流再搜集回来的进程,肯定会耗时,但是它占全进程的比率并不算高。

因此,这部只能说是小部份的原因。


排序进程


咱们能够经过以下源码很直观的看到。

为什么list.sort()比Stream().sorted()更快?

  • 1 begin办法初始化一个数组。
  • 2 accept 接收上游数据。
  • 3 end 办法开始进行排序。
    这儿第3步直接调用了原生的排序办法,完成排序后,第4步,遍历向下游发送数据。

所以经过源码,咱们也能很明显地看到,stream()排序所需时刻肯定是 > 原生排序时刻。

只不过,这儿要量化地搞理解,究竟多出了多少,这儿得去编译jdk源码,在第3步前后将时刻打印出来。

这一步我就不做了。
感兴趣的朋友能够去测一下。

不过我觉得这两点也能很好地回答,为什么list.sort()比Stream().sorted()更快。

补充阐明:

  1. 本文说的stream()流指的是串行流,而不是并行流。
  2. 绝大多数场景下,几百几千几万的数据,开心就好,怎样方便怎样用,没有必要去计较这点功能差异。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。