【问题标题】:Proper way of sorting Java beans by multiple fields按多个字段对 Java bean 进行排序的正确方法
【发布时间】:2013-11-01 02:48:55
【问题描述】:

我们的代码带有复杂的比较器,用于在整个应用程序中对 java 对象进行排序。从历史上看,这些都是有效的,但是自从在 Java 7 中引入 TimSort 以来,我们偶尔会遇到 Comparison 方法违反了它的一般约定! 错误.. 取决于对象中保存的数据。

这是我们的一个传统比较器的示例(可能已经有将近十年的历史了——请原谅我的狡猾):

 public int compare(TemplateBean b1, TemplateBean b2) {

  // avoid null pointer exceptions
  if (b1 == null && b2 == null) return 0;
  if (b1 == null) return 1;
  if (b2 == null) return -1;

  int cmp = 0;
  if ("UNATTACHED".equals(b1.getStatusCode()) &&
     !"UNATTACHED".equals(b2.getStatusCode())) {
     cmp = 1;
  }
  if (!"UNATTACHED".equals(b1.getStatusCode()) &&
     "UNATTACHED".equals(b2.getStatusCode())) {
     cmp = -1;
  }
  if (!"UNATTACHED".equals(b1.getStatusCode()) &&
     !"UNATTACHED".equals(b2.getStatusCode()) &&
     !"FIELDSIMPLE".equals(b1.getRefRltshpTypeCode()) &&
     !"FIELDSIMPLE".equals(b2.getRefRltshpTypeCode()) &&
     !"CUSTOM".equals(b1.getRefRltshpTypeCode()) &&
     !"CUSTOM".equals(b2.getRefRltshpTypeCode()) &&
     !"FUNCTION".equals(b1.getRefRltshpTypeCode()) &&
     !"FUNCTION".equals(b2.getRefRltshpTypeCode())) {
     String parent1 = b1.getGroupCode() == null ? "" : b1.getGroupCode().toUpperCase();
     String parent2 = b2.getGroupCode() == null ? "" : b2.getGroupCode().toUpperCase();
     cmp = parent1.compareTo(parent2);
  }

  if (cmp == 0) {
     Integer i1 = b1.getSortOrder() == null ? Const.ZERO : b1.getSortOrder();
     Integer i2 = b2.getSortOrder() == null ? Const.ZERO : b2.getSortOrder();
     cmp = i1.compareTo(i2);
  }

  if (cmp == 0) {
     String s1 = b1.getShortDescription();
     if (s1 == null) s1 = "";
     String s2 = b2.getShortDescription();
     if (s2 == null) s2 = "";
     cmp = s1.compareToIgnoreCase(s2);
  }

  return cmp;  }

所以,我想复制此功能,但使用一个可安全用于 TimSort 的 Comparator。

从代码中你可以看到这个比较有多个级别..

  1. 它将比较组代码。
  2. 如果组代码相同,它将比较排序顺序。
  3. 如果排序顺序相同,则会比较描述。

这意味着它将返回特定级别的比较结果。这可能是两个字符串或两个整数的比较结果。我认为这就是破坏 TimSort 的原因。

我能够让这个 Comparator 解决 General Contract 问题的唯一方法是对 bean 的内容进行哈希处理并执行字符串比较。其他想法包括编写我们自己的排序函数。肯定有更好的方法吗?

是否应该以另一种方式构造 bean 来支持这一点?

【问题讨论】:

  • 我对 TimSort 不是很熟悉,但可能对您有所帮助的一件事是为您正在比较的字段实现某种枚举,而不是尝试执行复杂的字符串比较操作。我不确定这是否是解决此问题的好方法,但这是我的一个想法。
  • 这里有太多的字符串比较。您真的应该将字符串转换为相应的数字或其他内容并进行比较,而不是试图挖掘这些混乱的条件。
  • 我不认为问题与如何确定比较(使用字符串或枚举)有关,我认为这与 TimSort 具有此限制的事实有关:实现者还必须确保关系是可传递的: ((compare(x, y)>0) && (compare(y, z)>0)) 意味着 compare(x, z)>0。 这意味着如果 X 和 Y 在同一级别进行比较,而 Y 和 Z 在不同级别进行比较,则返回的整数可能会违反此规则。我需要返回一个代表顺序的值,但也不违反 X > Y > Z 规则。
  • 乍一看,我会说问题出在第三个if(不包括空检查)内。最后两个似乎没有问题,因为它们使用字符串和整数比较,并且这些比较已经是传递的。等我有时间再仔细看看。

标签: java sorting comparator


【解决方案1】:

上述Comparator 的主要问题是它不具有传递性。它似乎在较旧的 JDK 上“工作”,因为它们没有提供对损坏的比较器的检测,但它在一般情况下无法正常工作,并且直到 JDK 7 才发现错误行为。

其非传递性的来源是在groupCode 属性的条件比较中。 考虑比较器将对象 A 和 B 排序为 A sortOrder 字段忽略了 groupCode 的比较,因为 "FUNCTION".equals(B.getRefRltshpTypeCode()) 和 由于sortOrder,对象 B 和 C 按 B groupCode 比较,直接比较时 A 和 C 可能按 C Comparator 的传递性要求。

要解决此问题,应始终考虑groupCode,并且由于refRltshpTypeCode 值而跳过groupCode 的每个对象都应被视为小于现在使用groupCode 的任何对象进行比较。

比较方法应该看起来像(这只是给你一个想法):

public int compare(TemplateBean b1, TemplateBean b2) {

    // avoid null pointer exceptions
    if (b1 == null && b2 == null) return 0;
    if (b1 == null) return 1;
    if (b2 == null) return -1;

    int cmp = 0;
    if ("UNATTACHED".equals(b1.getStatusCode()) &&
       !"UNATTACHED".equals(b2.getStatusCode())) {
        cmp = 1;
    }
    if (!"UNATTACHED".equals(b1.getStatusCode()) &&
       "UNATTACHED".equals(b2.getStatusCode())) {
       cmp = -1;
    }

    if (shouldBeComparenByGroupCode(b1) != shouldBeComparedByGroupCode(b2)) {
        if (!shouldBeComparenByGroupCode(b1)) {
            return -1;
        } else {
           return 1;
        }
    }

    if (shouldBeComparenByGroupCode(b1) && shouldBeComparenByGroupCode(b2)) {
        String parent1 = b1.getGroupCode() == null ? "" : b1.getGroupCode().toUpperCase();
        String parent2 = b2.getGroupCode() == null ? "" : b2.getGroupCode().toUpperCase();
        cmp = parent1.compareTo(parent2);
    }

    if (cmp == 0) {
        Integer i1 = b1.getSortOrder() == null ? Const.ZERO : b1.getSortOrder();
        Integer i2 = b2.getSortOrder() == null ? Const.ZERO : b2.getSortOrder();
        cmp = i1.compareTo(i2);
    }

    if (cmp == 0) {
        String s1 = b1.getShortDescription();
        if (s1 == null) s1 = "";
        String s2 = b2.getShortDescription();
        if (s2 == null) s2 = "";
        cmp = s1.compareToIgnoreCase(s2);
    }

    return cmp;
}

在哪里

private static boolean shouldBeComparenByGroupCode(TemplateBean b1) {
     return !"UNATTACHED".equals(b1.getStatusCode()) &&
            !"FIELDSIMPLE".equals(b1.getRefRltshpTypeCode()) &&
            !"CUSTOM".equals(b1.getRefRltshpTypeCode()) &&
            !"FUNCTION".equals(b1.getRefRltshpTypeCode());
}

【讨论】:

    【解决方案2】:

    来自@RomanKonovai 的answer 是正确的,但添加了更多细节。

    想想代码如何比较这三个对象,并假设所有的非引用:

                   A          B          C
    Status         UNATTACHED UNATTACHED UNATTACHED
    RefRltshpType  CUSTOM     FUNCTION   CUSTOM
    Group          Cat        Ball       Apple
    SortOrder      10         20         30
    

    通过问题中的实现,我们可以看到 A A < B < C < A,或A &lt; A。这显然是不合逻辑的,因为根据StatusRefRltshpType 的值,排序顺序由GroupSortOrder 确定,并且没有什么可以将这两者联系在一起。从本质上讲,这意味着您的排序顺序是未定义的,因为结果完全取决于输入的顺序,即sort(sort(List)) 可能不会给出与sort(List) 相同的结果。

    解决这个问题的方法是:

    private int objectCompare(String allowed, Comparable v1, Comparable v2) {
      if (v1 == v2) return 0;
      if (v1 == null) return 1;
      if (v2 == null) return -1;
      boolean c1 = v1.equals(allowed);
      boolean c2 = v2.equals(allowed);
      return c1 ? c2 ? 0 : 1 : c2 ? -1 : 0;
    }
    private int objectCompare(Comparable v1, Comparable v2) {
      if (v1 == v2) return 0;
      if (v1 == null) return 1;
      if (v2 == null) return -1;
      return v1.compare(v2);
    }
    public int compare(TemplateBean b1, TemplateBean b2) {
    
      // avoid null pointer exceptions
      if (b1 == b2) return 0;
      if (b1 == null) return 1;
      if (b2 == null) return -1;
    
      int cmp = objectCompare("UNATTACHED", b1.getStatusCode(), b2.getStatusCode());
      if (cmp == 0) {
        cmp = objectCompare("FIELDSIMPLE", b1.getRefRltshpTypeCode(), b2.getRefRltshpTypeCode());
        if (cmp == 0) {
          cmp = objectCompare("CUSTOM", b1.getRefRltshpTypeCode(), b2.getRefRltshpTypeCode());
          if (cmp == 0) {
            cmp = objectCompare("FUNCTION", b1.getRefRltshpTypeCode(), b2.getRefRltshpTypeCode());
            if (cmp == 0) {
              cmp = objectCompare(b1.getGroupCode(), b2.getGroupCode());
              if (cmp == 0) {
                cmp = objectCompare(b1.getSortOrder(), b2.getSortOrder());
                if (cmp == 0) {
                  cmp = objectCompare(b1.getShortDescription(), b2.getShortDescription());
                }
              }
            }
          }
        }
      }
    
      return cmp;
    }
    

    【讨论】:

    • 感谢您的精彩解释。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-10
    • 1970-01-01
    • 1970-01-01
    • 2021-11-05
    • 1970-01-01
    相关资源
    最近更新 更多