【问题标题】:Collections Sort unexpected behaviour集合排序意外行为
【发布时间】:2018-06-15 01:45:06
【问题描述】:

集合排序没有给我预期的结果,还是我误读了方法?

要排序的 Row 对象列表:

public class Row {

    private int id;
    private boolean line;

    public Row(int id, boolean line) {
        this.id = id;
        this.line = line;
    }

    public boolean isLine() {
        return line;
    }

    @Override public String toString() {
        return "Row{" + "id=" + id + ", line=" + line + '}';
    }
}

起始数据:

[Row{id=0, line=true}, Row{id=1, line=false}, Row{id=2, line=true}, Row{id=3, line=false}]

排序代码:

    Collections.sort(rows, new Comparator<Row>(){
        @Override public int compare(Row o1, Row o2) {
            if (!o1.isLine() && !o2.isLine()) return 0;
            if (o1.isLine()) {
                    return 1;
            } else {
                    return -1;
            }
        }
    });

结果:

[Row{id=1, line=false}, Row{id=3, line=false}, Row{id=0, line=true}, Row{id=2, line=true}]

我的印象是所有带有line=true 的对象都应该在列表的开头,而不是结尾。

如果我稍微改变 Comporator 的实现:

    Collections.sort(rows, new Comparator<Row>(){
        @Override public int compare(Row o1, Row o2) {
            if (!o1.isLine() && !o2.isLine()) return 0;
            if (o1.isLine()) {
                return -1;
            } else {
                return 1;
            }
        }
    });

结果:

[Row{id=2, line=true}, Row{id=0, line=true}, Row{id=1, line=false}, Row{id=3, line=false}]

现在可以在列表的开头找到所有带有line=true 的对象,但它们已经交换了位置(id=0 应该是第一个)。

预期的排序结果:

[Row{id=0, line=true}, Row{id=2, line=true}, Row{id=1, line=false}, Row{id=3, line=false}]

【问题讨论】:

  • 你的比较器也应该考虑id
  • 如果compare(o1, o2)返回1,则表示o1更大。第一次调用的结果完全符合。
  • 你的第一个 if 语句应该是 if (o1.isLine() == o2.isLine()) - 否则,如果两者都是真的,你的排序有时会返回 1,有时会返回 -1(当它应该一直返回 0 时)。但实际上您可以将整个方法替换为 !Boolean.compare(o1.isLine(), o2.isLine())
  • @CrazySabbath 如果您觉得对未来的访问者有帮助,欢迎您自己在答案中发布我的 cmets。
  • 排序是从低到高。

标签: java sorting


【解决方案1】:

我的印象是所有带有 line=true 的对象都应该在 列表的开头,而不是结尾。

不像这段代码:

  if (o1.isLine()) {
       return 1;
  } 

表示o1优于o2
因此,isLine=true 的对象将发生在末尾,因为默认顺序是升序。

所有 line=true 的对象都可以在列表的开头找到 现在,但他们已经交换了位置(id=0 应该是第一个)。

您永远不会在比较器实现中使用id
绝对不能考虑。

获取:

[行{id=0,行=真},行{id=2,行=真},行{id=1,行=假}, 行{id=3, line=false}]

您应该在Row 中添加一个getter 来检索id。 那么您应该首先按line=trueid ASC 排序。

 Collections.sort(rows, new Comparator<Row>(){
    @Override public int compare(Row o1, Row o2) {
        if (!o1.isLine() && !o2.isLine()) return 0;
        if (o1.isLine() && o2.isLine()) {
            return o1.getId() > o2.getId();
        }
        if (o1.isLine()) {
            return -1;
        } else {
            return 1;
        }
    }
 });

更简洁的编写方法是使用 Java 8 Comparator:

Comparator<Row> comparatorRow = Comparator.comparing(Row::isLine).reversed()
                                          .thenComparing(Row::getId);

由于行已按 id 排序,并且排序保证稳定:排序结果不会重新排序相等的元素,您只能在 isLine 上进行比较:

Comparator<Row> comparatorRow = Comparator.comparing(Row::isLine).reversed();

【讨论】:

  • 不,我不依赖它,首先 if 语句确保了这一点。为什么这甚至被赞成?
  • @CrazySabbath 这很奇怪——如果两行都是假的,那么我们不关心 ids,但如果两者都是真的,我们会关心吗?如果主排序标准相等,则应遵循次要排序标准。
  • laune 你说得对,我总结了@Dukeling cmets,效果很好。
【解决方案2】:

您的比较不是对称的,因此被破坏了。使用在 Boolean 类中实现的反向比较(请注意 lambda 正文开头的 minus):

Collections.sort(rows, (o1, o2) -> -Boolean.valueOf(o1.isLine()).compareTo(o2.isLine()));

【讨论】:

  • 我想你的意思是Boolean.compare(...)Boolean.valueOf(...).compareTo(...)
【解决方案3】:

总结@Dukeling:

    Collections.sort(rows, new Comparator<Row>(){
        @Override public int compare(Row o1, Row o2) {
            return -Boolean.compare(o1.isLine(), o2.isLine());
        }
    });

这给出了预期的结果。

输入:

[Row{id=0, line=true}, Row{id=1, line=false}, Row{id=2, line=true}, Row{id=3, line=false}]

结果:

[Row{id=0, line=true}, Row{id=2, line=true}, Row{id=1, line=false}, Row{id=3, line=false}]

【讨论】:

  • 在您的实现中不考虑 id 排序。所以预期很好,但这只是机会。在列表中添加更多对象,并且 id 不应按升序排序。
  • @davidxxx 尝试了 1000 个对象,所有 Id 都按升序排列。
  • 感谢您的反馈。我想在调用sort() 之前,id 已经在列表中排序,并且 sort() 实现使得在以迭代方式处理时保持顺序。但这不是定义比较器的可靠方法。该算法可能会在运行时或未来的 JDK 版本中发生变化。
  • @davidxxx Collections.sort 使用稳定的排序算法,这是由 Javadoc 保证的。
  • @Klitos Kyriacou 你有参考吗?