向比较器的比较方法添加额外的规则

Add additional rules to the compare method of a Comparator

我目前有一个代码片段,其中 returns 个列表的字符串按升序排列:

Collections.sort(myList, new Comparator<MyClass>() {
    @Override
    public int compare(MyClass o1, MyClass o2) {
        return o1.aString.compareTo(o2.aString);
    }
});

虽然可行,但我想在顺序中添加一些自定义 "rules" 以将某些字符串放在前面。例如:

if(aString.equals("Hi")){
// put string first in the order
}

if(aString begins with a null character, e.g. " ") {
// put string after Hi, but before the other strings
}

// So the order could be: Hi, _string, a_string, b_string, c_string

是否可以像这样使用 Comparator 自定义列表的排序?

是的,这是可能的,您可以完全控制 compareTo() 方法。两件事:

  1. 使用 String#equals 而不是 == 来比较字符串
  2. 请确保针对异常情况检查 compareTo 的两个参数。

一种具体的实现方式,其中一些词总是在前面,而一些词总是在最后,在例外中定义了顺序:

Map<String, Integer> exceptionMap = new HashMap<>();
exceptionMap.put("lowest", -2);
exceptionMap.put("second_lowest", -1);
exceptionMap.put("second_highest", 1);
exceptionMap.put("highest", 2);

public int compareToWithExceptionMap(String s1, String s2) {
  int firstExceptional = exceptionMap.getOrDefault(s1, 0);
  int secondExceptional = exceptionMap.getOrDefault(s2, 0);

  if (firstExceptional == 0 && secondExceptional == 0) {
    return s1.compareTo(s2);
  }
  return firstExceptional - secondExceptional;
}

这是可能的。

使用 Java 8 个特征

您可以将一个函数传递给 Comparator.comparing 方法来定义您的规则。请注意,我们只是 return 整数,元素的最低整数应该排在第一位。

Comparator<MyClass> myRules = Comparator.comparing(t -> {
    if (t.aString.equals("Hi")) {
        return 0;
    }
    if (t.aString.startsWith(" ")) {
        return 1;
    }
    else {
        return 2;
    }
});

如果您希望剩余的元素按字母顺序排序,您可以使用 thenComparing(Comparator.naturalOrder()),如果您的 class 实现了 Comparable。否则,您应该先提取排序键:

Collections.sort(myList, myRules.thenComparing(Comparator.comparing(t -> t.aString)));

请注意,实际的特定数字 returned 并不重要,重要的是排序时较低的数字在较高的数字之前,因此如果总是将字符串 "Hi" 放在第一位,那么相应的数字应该是最低的returned(在我的例子中是0)。

使用 Java <= 7 个功能(Android API 21 级兼容)

如果Java 8 项功能您无法使用,那么您可以这样实现:

Comparator<MyClass> myRules = new Comparator<MyClass>() {

    @Override
    public int compare(MyClass o1, MyClass o2) {
        int order = Integer.compare(getOrder(o1), getOrder(o2));
        return (order != 0 ? order : o1.aString.compareTo(o2.aString));
    }

    private int getOrder(MyClass m) {
        if (m.aString.equals("Hi")) {
            return 0;
        }
        else if (m.aString.startsWith(" ")) {
            return 1;
        }
        else {
            return 2;
        }
    }
};

并这样称呼它:

Collections.sort(list, myRules);

其工作原理如下:首先,两个接收到的字符串都映射到您的自定义规则集并相互相减。如果两者不同,则操作 getOrder(o1) - getOrder(o2) 确定比较。否则,如果两者相同,则按字典顺序进行比较。

Here is some code in action.

来自 MC Emperor 的 非常好 (+1),因为它满足了 OP 不使用 Java 8 个 API 的要求。它还使用一种简洁的内部函数技术(getOrder 方法)将条件映射到小整数值以实现一级比较。

这是一个使用 Java 8 构造的替代方案。它假定 MyClass 有一个 getString 方法可以完成显而易见的事情。

    Collections.sort(myList,
        Comparator.comparing((MyClass mc) -> ! mc.getString().equals("Hi"))
                  .thenComparing(mc -> ! mc.getString().startsWith(" "))
                  .thenComparing(MyClass::getString));

在您习惯这种风格之前,这是非常不透明的。关键见解是提供给 Comparator.comparingComparator.thenComparing 的 "extractor" 函数通常只是提取一个字段,但它可以是到任何其他值的一般映射。如果该值是 Comparable 则不需要为其提供额外的 Comparator。在这种情况下,提取器函数是一个布尔表达式。这被装箱到一个布尔值,结果是 Comparable。由于 false 订单在 true 之前,我们需要取反布尔表达式。

另请注意,我必须为 lambda 参数提供显式类型声明,因为类型推断通常不适用于链式比较器情况,例如这个。