在比较器中使用列表时,对ArrayList进行排序可能会失败。有记录吗?

时间:2019-01-06 11:37:14

标签: java comparator timsort

ArrayLists似乎是用TimSort排序的,其中底层列表在排序过程中并不总是一致的。调用比较器时,列表条目可能会消失或出现两次。

在比较器中,我们正在比较使用函数的键,以获取要对此键进行比较的值。由于在其他上下文中使用了此函数,因此我们要测试密钥是否真正存在于列表中(排序中不需要的东西):

        if (keys.contains(itemId)) {
          ...

由于 keys 是我们正在排序的列表,由于TimSort的内部机制,在比较器中可能会在列表中找不到密钥。

问题:在Javadoc中某个地方(找不到它)提到您不应该访问Comparator中的基础列表吗?这是TimSort的较差实现,应该对副本进行排序吗?还是首先访问比较器中的底层列表是一个愚蠢的主意?


T.J. Crowder提供的以下程序演示了在调用比较器期间底层列表的内容可能不一致。 (该程序演示了有问题的现象,但并不代表受该问题影响的实际应用程序。)

import java.util.*;

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

    private List<String> list;
    private String[] entries;

    private Example() {
        this.entries = new String[1000];
        for (int n = 0; n < 1000; ++n) {
            this.entries[n] = chars[n % chars.length] + n;
        }
        // Ensure it's an ArrayList, specifically
        this.list = new ArrayList<String>(Arrays.asList(this.entries));
    }

    public static void main(String[] args) {
        (new Example()).run();
    }

    class ListComparator implements Comparator<String> {
        public int compare(String a, String b) {
            for (String s : entries) {
                int i1 = Example.this.list.indexOf(s);
                if (i1 == -1) {
                    System.out.println(s + ": Missing");
                } else {
                    int i2 = Example.this.list.lastIndexOf(s);
                    if (i2 != i1) {
                        System.out.println(s + ": Duplicated, at " + i1 + " and " + i2);
                    }
                }
            }
            return a.compareTo(b);
        }
    }

    private void run() {
        this.list.sort(new ListComparator());
    }
}

这是运行的前几行输出:

b1: Missing
a52: Duplicated, at 2 and 32
b27: Missing
a52: Duplicated, at 2 and 32
c2: Missing
a52: Duplicated, at 2 and 32
c2: Missing
c28: Missing
a52: Duplicated, at 2 and 32
b53: Duplicated, at 5 and 33
c28: Missing
d29: Missing
a52: Duplicated, at 2 and 32
b53: Duplicated, at 5 and 33
d3: Missing
d29: Missing
a52: Duplicated, at 2 and 32
b53: Duplicated, at 5 and 33
d3: Missing
d29: Missing
e30: Missing

1 个答案:

答案 0 :(得分:2)

这里有一段历史:在JDK 7中,TimSort算法取代了以前的“旧版合并排序”算法。在JDK 8中,Collections.sort()委托给新的默认方法List.sort()ArrayList覆盖了此默认方法,该方法就地进行排序。先前的Collections.sort()实现将列表复制到一个临时数组,对该临时数组执行排序,然后将元素从该临时数组复制回原始列表。

如果排序比较器在要排序的列表中查找,那么它的行为肯定会受到JDK 8中引入的ArrayList新的就地排序行为的影响。从“旧式合并排序”到JDK 7中的TimSort的更改在这种情况下可能没有影响,因为JDK 7仍对临时副本进行了排序。

“实施要求”部分描述了List.sort()的copy-sort-copyback行为,该行为指定了默认方法实现的行为,但这不是强加给所有接口的接口契约的一部分实现。因此,ArrayList(和其他子类)可以自由更改此行为。我注意到没有关于重写实现ArrayList.sort()的文档。我想,如果添加一些文档来指定就地排序行为,那将是一个小改进。

如果对ArrayList进行就地排序有问题,则可以在对列表进行排序之前复制列表:

List<Key> list = ... ;
List<Key> newList = new ArrayList<>(list);
newList.sort(keyComparator); // uses the old list
list = newList;

或者,也许您可​​以提供更多有关比较器工作方式的详细信息,并且我们也许能够找到一种重写它的方法,这样它就不必查看正在排序的列表。 (我建议对此再问一个问题。)