通过多个字段对Java bean进行排序的正确方法

时间:2013-10-23 01:42:08

标签: java sorting comparator

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

这是我们的一个传统比较器的例子(可能差不多十年了 - 请原谅这个狡猾的人):

 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. 如果排序顺序相同,则会比较说明。
  4. 这意味着它将在特定级别返回比较结果。这可能是两个字符串或两个整数的比较结果。我认为这就是打破TimSort的原因。

    我能够使此Comparator解决一般合同问题的唯一方法是对bean的内容进行散列并执行字符串比较。其他想法包括编写我们自己的排序功能..当然有更好的方法吗?

    是否应该以另一种方式构建bean来支持它?

2 个答案:

答案 0 :(得分:3)

上述Comparator的主要问题是它不具有传递性。它可能似乎在较旧的JDK上“工作”,因为它们没有为破坏的比较器提供检测,但它在一般情况下无法正常工作,并且在JDK 7之前没有显示错误行为。

其非传递性的来源是groupCode属性的条件比较。 当比较器将对象A和B命令为A< B由于sortOrder字段省略了groupCode的比较,因为"FUNCTION".equals(B.getRefRltshpTypeCode())和 对象B和C按B sortOrder引起。但是,当直接比较时,A和C可能被命令为C<由于groupCode比较而导致的。这打破了Comparator的传递要求。

要解决此问题,应始终考虑groupCode,并且应该将因groupCode值而跳过refRltshpTypeCode的每个对象视为小于任何对象的对象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());
}

答案 1 :(得分:2)

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

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

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

通过问题中的实施,我们可以看到A&lt; B,B&lt; C和C&lt;答:换句话说,A < B < C < AA < 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;
}