item 14: 考虑实现 COMPARABLE

2019-05-14  本文已影响0人  rabbittttt

ITEM 14: CONSIDER IMPLEMENTING COMPARABLE
  Object 中 并没有定义 compareTo 方法,而是将它作为一个接口 Comparable。看起来,compareTo 似乎与 equal 方法拥有同样的用途,只是除了简单的相等比较之外,它还允许顺序比较,而且它是通用的。通过实现 Comparable,类表明其实例具有自然顺序。对实现 Comparable 的对象数组排序非常简单:
Arrays.sort(a);
  同样,搜索、计算极值和维护可比较对象的自动排序集合也很容易。例如,下面的程序,它依赖于 String 实现 Comparable 的事实,打印了一个按字母顺序排列的命令行参数列表,去掉了重复项:

public class WordList {
  public static void main(String[] args) {
    Set<String> s = new TreeSet<>();
    Collections.addAll(s, args); 
    System.out.println(s);
  } 
}

  通过实现Comparable,您将允许类与依赖于这个接口的所有通用算法和集合实现进行互操作。你只需付出一点点努力就能获得巨大的力量。实际上,Java平台库中的所有值类以及所有 enum 类型都实现了Comparable。如果您编写的值类具有明显的自然顺序,如字母顺序、数字顺序或时间顺序,您应该实现可比较的接口:

public interface Comparable<T> {
  int compareTo(T t);
}

  compareTo 方法的规范如下:
  将此对象与指定的对象进行比较。返回一个负整数、零或正整数,因为该对象小于、等于或大于指定的对象。
  如果指定对象的类型阻止将其与此对象进行比较,则引发ClassCastException。
  在下面的描述中,符号 sgn(expression) 指定了数学 sgn 函数,根据表达式的值是负、零还是正,sgn函数被定义为返回 -1, 0 或 1。

// Single-field Comparable with object reference field
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
  public int compareTo(CaseInsensitiveString cis) {
    return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
  }
... 
// Remainder omitted 
}

  注意,CaseInsensitiveString 实现了 Comparable<CaseInsensitiveString>。这意味着一个 CaseInsensitiveString 引用只能与另一个 CaseInsensitiveString 引用进行比较。这是在声明要实现 Comparable 类时要遵循的常规模式。
本书以前的版本建议 compareTo 方法使用关系运算符 < 和 > 比较整数原语字段,使用静态方法 Double.compare 和 Float.compare 比较浮点原语字段。在Java 7中,静态比较方法被添加到所有Java的装箱原始类中。在compareTo方法中使用关系运算符 < 和 > 冗长且容易出错,不再推荐使用。
  如果一个类有多个重要字段,比较它们的顺序是至关重要的。从最重要的领域开始,然后慢慢向下。如果比较结果不是0(代表相等),那么就完成了;只返回结果。如果最重要的字段是相等的,比较下一个最重要的字段,以此类推,直到找到一个不相等的字段或比较最不重要的字段。下面是 item 11 中 PhoneNumber 类的 compareTo 方法,演示了这种技术:

public int compareTo(PhoneNumber pn) {
  int result = Short.compare(areaCode, pn.areaCode); 
  if (result == 0) {
    result = Short.compare(prefix, pn.prefix); 
    if (result == 0)
      result = Short.compare(lineNum, pn.lineNum); 
  }
  return result; 
}

  在Java 8中,接口 Comparator 配备了一组比较器构造方法,这使得 Comparator 的构造更加流畅。然后,可以根据 Comparable 接口的要求,使用这些比较器实现compareTo 方法。许多程序员更喜欢这种方法的简明性,尽管它的性能不太好:在我的机器上,对 PhoneNumber 实例数组排序的速度要慢10%左右。当使用这种方法时,请考虑使用Java的静态导入工具,这样您就可以通过静态比较器的简单名称引用静态比较器构造方法,以保持清晰和简洁。下面是使用这种方法的PhoneNumber的compareTo方法:

// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR = 
  comparingInt((PhoneNumber pn) -> pn.areaCode)
     .thenComparingInt(pn -> pn.prefix)
     .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) { 
    return COMPARATOR.compare(this, pn);
}

  这个实现使用两个比较器构造方法在类初始化时构建一个比较器。第一个是比较int。它是一个静态方法,接受一个键提取器函数,该函数将对象引用映射到int类型的键,并返回一个比较器,该比较器根据该键对实例排序。在前面的示例中,comparingInt使用lambda() 从 PhoneNumber 中提取区号,并返回一个 Comparator,该比较器根据电话区号排序电话号码。注意,lambda 显式地指定其输入参数的类型(PhoneNumber pn)。事实证明,在这种情况下,Java的类型推断并没有强大到足以自己找出类型,因此我们不得不帮助它来编译程序。
  如果两个电话号码有相同的区号,我们需要进一步细化比较,这正是第二个比较器构造方法,thenComparingInt 所做的。它是 Comparator 上的一个实例方法,它接受一个 int 键提取器函数,并返回一个比较器,该比较器首先应用原始比较器,然后使用提取的键断开连接。您可以根据自己的喜好对 thenComparingInt 进行尽可能多的调用,从而实现字典顺序。在上面的例子中,我们将两个对 thenComparingInt 的调用叠加在一起,得到一个顺序,其中次级键是前缀,第三个键是行号。注意,我们不必指定传递给对 thenComparingInt 的两个调用中的任何一个的键提取器函数的参数类型:Java的类型推断足够聪明,可以自己解决这个问题。
  Comparator 类有完整的构造方法。对于 long 和 double 原始类型 。int版本还可以用于更窄的整数类型,例如short,如我们的PhoneNumber示例。double 版本也可以用于 float。这提供了对所有Java数字基本类型的覆盖。
  对象引用类型也有比较器构造方法。名为 compare 的静态方法有两个重载。一个使用 key 并使用 key 的自然顺序。第二种方法同时使用 key 和比较器对提取的 key 进行比较。实例方法有三种重载,称为thenComparison。一个重载只需要一个比较器并使用它来提供二级顺序。第二次重载只使用密钥提取器,并使用密钥的自然顺序作为次要顺序。最后的重载需要一个密钥提取器和一个比较器来对提取的密钥进行使用。
  有时候,您可能会看到compareTo或compare方法,它们依赖于这样一个事实:如果第一个值小于第二个值,那么两个值之间的差为负:如果两个值相等,那么差为零;如果第一个值更大,那么差为正。举个例子:

// BROKEN difference-based comparator - violates transitivity!
static Comparator<Object> hashCodeOrder = new Comparator<>() {
  public int compare(Object o1, Object o2) { 
    return o1.hashCode() - o2.hashCode();
  } };

  不要使用这种技术。它充满了整数溢出和 IEEE 754 浮点运算伪码的危险。此外,生成的方法不太可能比使用本项目中描述的技术编写的方法快。使用静态比较方法:

// Comparator based on static compare method
static Comparator<Object> hashCodeOrder = new Comparator<>() {
  public int compare(Object o1, Object o2) {
    return Integer.compare(o1.hashCode(), o2.hashCode());
  } };

  或比较器构造方法:

// Comparator based on Comparator construction method
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

  总之,无论何时实现具有合理排序的值类,都应该让该类实现 Comparable 接口,以便能够轻松地对其实例进行排序、搜索,并在基于比较的集合中使用。在比较compareTo 方法实现中的字段值时,要避免使用 < 和 > 操作符。相反,使用装箱的基本类中的静态比较方法或比较器接口中的比较器构造方法。

上一篇下一篇

猜你喜欢

热点阅读