什么更有效:排序流或排序列表?

时间:2018-04-12 13:40:46

标签: java arraylist java-8 java-stream

假设我们在集合中有一些项目,我们希望使用某个比较器对它们进行排序,期望在列表中得到结果:

Collection<Item> items = ...;
Comparator<Item> itemComparator = ...;

其中一种方法是对列表中的项目进行排序,例如:

List<Item> sortedItems = new ArrayList<>(items);
Collections.sort(sortedItems, itemComparator);

另一种方法是使用排序流:

List<Item> sortedItems = items
    .stream()
    .sorted(itemComparator)
    .collect(Collectors.toList());

我想知道,哪种方法效率更高?排序流是否有任何优点(如多核上的紧固排序)?

从运行时复杂度/最快的角度来看,效率很高。

我不相信自己能够实现一个完美的benchmark并且学习SortedOps并没有真正启发我。

3 个答案:

答案 0 :(得分:11)

说实话,我不相信自己太多JMH中(除非我理解大会,这需要花费很多时间),特别是因为我已经使用了@Setup(Level.Invocation),但是这是一个小测试(我从其他测试中获取了StringInput代,但它没关系,它只是一些要排序的数据)

@State(Scope.Thread)
public static class StringInput {

    private String[] letters = { "q", "a", "z", "w", "s", "x", "e", "d", "c", "r", "f", "v", "t", "g", "b",
            "y", "h", "n", "u", "j", "m", "i", "k", "o", "l", "p" };

    public String s = "";

    public List<String> list;

    @Param(value = { "1000", "10000", "100000" })
    int next;

    @TearDown(Level.Invocation)
    public void tearDown() {
        s = null;
    }

    @Setup(Level.Invocation)
    public void setUp() {

         list = ThreadLocalRandom.current()
                .ints(next, 0, letters.length)
                .mapToObj(x -> letters[x])
                .map(x -> Character.toString((char) x.intValue()))
                .collect(Collectors.toList());

    }
}


@Fork(1)
@Benchmark
public List<String> testCollection(StringInput si){
    Collections.sort(si.list, Comparator.naturalOrder());
    return si.list;
}

@Fork(1)
@Benchmark
public List<String> testStream(StringInput si){
    return si.list.stream()
            .sorted(Comparator.naturalOrder())
            .collect(Collectors.toList());
}

结果显示Collections.sort更快,但不是很大:

Benchmark                                 (next)  Mode  Cnt   Score   Error  Units
streamvsLoop.StreamVsLoop.testCollection    1000  avgt    2   0.038          ms/op
streamvsLoop.StreamVsLoop.testCollection   10000  avgt    2   0.599          ms/op
streamvsLoop.StreamVsLoop.testCollection  100000  avgt    2  12.488          ms/op
streamvsLoop.StreamVsLoop.testStream        1000  avgt    2   0.048          ms/op
streamvsLoop.StreamVsLoop.testStream       10000  avgt    2   0.808          ms/op
streamvsLoop.StreamVsLoop.testStream      100000  avgt    2  15.652          ms/op

答案 1 :(得分:7)

可以肯定地说,两种排序形式具有相同的复杂性......即使不查看代码也是如此。 (如果他们没有,那么一个表格会严重破坏!)

查看流的Java 8源代码(特别是内部类java.util.stream.SortedOps),sorted()方法将一个组件添加到流管道中,该流管道将所有流元素捕获到一个数组或一个数组中ArrayList

  • 当且仅当管道汇编代码可以提前推断出流中的元素数时,才使用数组。

  • 否则,ArrayList用于收集要排序的元素。

如果使用ArrayList,则会产生构建/增长列表的额外开销。

然后我们返回两个版本的代码:

List<Item> sortedItems = new ArrayList<>(items);
Collections.sort(sortedItems, itemComparator);

在此版本中,ArrayList构造函数将元素items复制到适当大小的数组,然后Collections.sort执行该数组的就地排序。 (这发生在封面下)。

List<Item> sortedItems = items
    .stream()
    .sorted(itemComparator)
    .collect(Collectors.toList());

在这个版本中,正如我们在上面看到的那样,与sorted()相关联的代码可以构建和排序数组(相当于上面发生的情况),或者以慢速方式构建ArrayList。但最重要的是,从items和收集器传输数据的开销很大。

总体而言(至少使用Java 8实现)代码检查告诉我,代码的第一个版本不能比第二个版本慢,并且在大多数(如果不是全部)情况下它会更快。但随着列表变大,O(NlogN)排序将主导复制的O(N)开销。这意味着两个版本之间的相对差异会变小。

如果您真的在意,您应该能够编写基准来测试Java的特定实现和特定输入数据集的实际差异。 (或者适应@Eugene的基准!)

答案 2 :(得分:1)

以下是我的基准(不确定它是否正确):

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(MyBenchmark.N)
public class MyBenchmark {

    public static final int N = 50;

    public static final int SIZE = 100000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        System.out.println("Generating the list");
        for (int i = 0; i < SIZE; i++) {
            sourceList.add(i);
        }
        System.out.println("Shuffling the list.");
        Collections.shuffle(sourceList);
    }

    @Benchmark
    public List<Integer> sortingList() {
        List<Integer> sortedList = new ArrayList<>(sourceList);
        Collections.sort(sortedList);
        return sortedList;
    }

    @Benchmark
    public List<Integer> sortedStream() {
        List<Integer> sortedList = sourceList.stream().sorted().collect(Collectors.toList());
        return sortedList;
    }

    @Benchmark
    public List<Integer> treeSet() {
        Set<Integer> sortedSet = new TreeSet<>(sourceList);
        List<Integer> sortedList = new ArrayList<>(sortedSet);
        return sortedList;
    }
}

结果:

Benchmark                 Mode  Cnt       Score       Error  Units
MyBenchmark.sortedStream  avgt  200  300691.436 ± 15894.717  ns/op
MyBenchmark.sortingList   avgt  200  262704.939 ±  5073.915  ns/op
MyBenchmark.treeSet       avgt  200  856577.553 ± 49296.565  ns/op

与@ Eugene的基准测试一样,排序列表比排序流程略快(约20%)。令我感到震惊的是,treeSet明显变慢了。我没想到。