Effective Java 第三版——14.考虑实现Comparable接口

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。程序员

Effective Java, Third Edition

 14.考虑实现Comparable接口

与本章讨论的其余方法不一样,compareTo方法并无在Object类中声明。 相反,它是Comparable接口中的惟一方法。 它与Object类的equals方法在性质上是类似的,除了它容许在简单的相等比较以外的顺序比较,它是泛型的。 经过实现Comparable接口,一个类代表它的实例有一个天然顺序( natural ordering)。 对实现Comparable接口的对象数组排序很是简单,以下所示:算法

Arrays.sort(a);

它很容易查找,计算极端数值,以及维护Comparable对象集合的自动排序。例如,在下面的代码中,依赖于String类实现了Comparable接口,去除命令行参数输入重复的字符串,并按照字母顺序排序:express

public class WordList {

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

经过实现Comparable接口,可让你的类与全部依赖此接口的通用算法和集合实现进行互操做。 只需少许的努力就能够得到巨大的能量。 几乎Java平台类库中的全部值类以及全部枚举类型(条目 34)都实现了Comparable接口。 若是你正在编写具备明显天然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现Comparable接口:数组

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

compareTo方法的通用约定与equals类似:函数

将此对象与指定的对象按照排序进行比较。 返回值可能为负整数,零或正整数,由于此对象对应小于,等于或大于指定的对象。 若是指定对象的类型与此对象不能进行比较,则引起ClassCastException异常。性能

下面的描述中,符号sgn(expression)表示数学中的 signum 函数,它根据表达式的值为负数、零、正数,对应返回-一、0和1。学习

  • 实现类必须确保全部xy都知足sgn(x.compareTo(y)) == -sgn(y. compareTo(x))。 (这意味着当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)必须抛出异常。)
  • 实现类还必须确保该关系是可传递的:(x. compareTo(y) > 0 && y.compareTo(z) > 0)意味着x.compareTo(z) > 0
  • 最后,对于全部的z,实现类必须确保[x.compareTo(y) == 0意味着sgn(x.compareTo(z)) == sgn(y.compareTo(z))测试

  • 强烈推荐x.compareTo(y) == 0) == (x.equals(y)),但不是必需的。 通常来讲,任何实现了Comparable接口的类违反了这个条件都应该清楚地说明这个事实。 推荐的语言是“注意:这个类有一个天然顺序,与equals不一致”。this

equals方法同样,不要被上述约定的数学特性所退缩。这个约定并不像看起来那么复杂。 与equals方法不一样,equals方法在全部对象上施加了全局等价关系,compareTo没必要跨越不一样类型的对象:当遇到不一样类型的对象时,compareTo被容许抛出ClassCastException异常。 一般,这正是它所作的。 约定确实容许进行不一样类型间比较,这种比较一般在由被比较的对象实现的接口中定义。命令行

正如一个违反hashCode约定的类可能会破坏依赖于哈希的其余类同样,违反compareTo约定的类可能会破坏依赖于比较的其余类。 依赖于比较的类,包括排序后的集合TreeSetTreeMap类,以及包含搜索和排序算法的实用程序类CollectionsArrays

咱们来看看compareTo约定的规定。 第一条规定,若是反转两个对象引用之间的比较方向,则会发生预期的事情:若是第一个对象小于第二个对象,那么第二个对象必须大于第一个; 若是第一个对象等于第二个,那么第二个对象必须等于第一个; 若是第一个对象大于第二个,那么第二个必须小于第一个。 第二项约定说,若是一个对象大于第二个对象,而第二个对象大于第三个对象,则第一个对象必须大于第三个对象。 最后一条规定,全部比较相等的对象与任何其余对象相比,都必须获得相同的结果。

这三条规定的一个结果是,compareTo方法所实施的平等测试必须遵照equals方法约定所施加的相同限制:自反性,对称性和传递性。 所以,一样须要注意的是:除非你愿意放弃面向对象抽象(条目 10)的好处,不然没法在保留compareTo约定的状况下使用新的值组件继承可实例化的类。 一样的解决方法也适用。 若是要将值组件添加到实现Comparable的类中,请不要继承它;编写一个包含第一个类实例的不相关的类。 而后提供一个返回包含实例的“视图”方法。 这使你能够在包含类上实现任何compareTo方法,同时客户端在须要时,把包含类的实例视同以一个类的实例。

compareTo约定的最后一段是一个强烈的建议,而不是一个真正的要求,只是声明compareTo方法施加的相等性测试,一般应该返回与equals方法相同的结果。 若是遵照这个约定,则compareTo方法施加的顺序被认为与equals相一致。 若是违反,顺序关系被认为与equals不一致。 其compareTo方法施加与equals不一致顺序关系的类仍然有效,但包含该类元素的有序集合可能不服从相应集合接口(CollectionSetMap)的通常约定。 这是由于这些接口的通用约定是用equals方法定义的,可是排序后的集合使用compareTo强加的相等性测试来代替equals。 若是发生这种状况,虽然不是一场灾难,但还是一件值得注意的事情。

例如,考虑BigDecimal类,其compareTo方法与equals不一致。 若是你建立一个空的HashSet实例,而后添加new BigDecimal("1.0")new BigDecimal("1.00"),则该集合将包含两个元素,由于与equals方法进行比较时,添加到集合的两个BigDecimal实例是不相等的。 可是,若是使用TreeSet而不是HashSet执行相同的过程,则该集合将只包含一个元素,由于使用compareTo方法进行比较时,两个BigDecimal实例是相等的。 (有关详细信息,请参阅BigDecimal文档。)

编写compareTo方法与编写equals方法相似,可是有一些关键的区别。 由于Comparable接口是参数化的,compareTo方法是静态类型的,因此你不须要输入检查或者转换它的参数。 若是参数是错误的类型,那么调用将不会编译。 若是参数为null,则调用应该抛出一个NullPointerException异常,而且一旦该方法尝试访问其成员,它就会当即抛出这个异常。

compareTo方法中,比较属性的顺序而不是相等。 要比较对象引用属性,请递归调用compareTo方法。 若是一个属性没有实现Comparable,或者你须要一个非标准的顺序,那么使用Comparator接口。 能够编写本身的比较器或使用现有的比较器,如在条目 10中的CaseInsensitiveString类的compareTo方法中:

// 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](http://ORDER.compare(s), cis.s);
    }
    ... // Remainder omitted
}

请注意,CaseInsensitiveString类实现了Comparable <CaseInsensitiveString>接口。 这意味着CaseInsensitiveString引用只能与另外一个CaseInsensitiveString引用进行比较。 当声明一个类来实现Comparable接口时,这是正常模式。

在本书第二版中,曾经推荐若是比较整型基本类型的属性,使用关系运算符“<” 和 “>”,对于浮点类型基本类型的属性,使用Double.compare和[Float.compare静态方法。在Java 7中,静态比较方法被添加到Java的全部包装类中。 在compareTo方法中使用关系运算符“<” 和“>”是冗长且容易出错的,再也不推荐。

若是一个类有多个重要的属性,那么比较他们的顺序是相当重要的。 从最重要的属性开始,逐步比较全部的重要属性。 若是比较结果不是零(零表示相等),则表示比较完成; 只是返回结果。 若是最重要的字段是相等的,比较下一个重要的属性,依此类推,直到找到不相等的属性或比较剩余不那么重要的属性。 如下是条目 11中PhoneNumber类的compareTo方法,演示了这种方法:

// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
    int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
    if (result == 0)  {
        result = [Short.compare(prefix](http://Short.compare(prefix), pn.prefix);
        if (result == 0)
            result = [Short.compare(lineNum](http://Short.compare(lineNum), pn.lineNum);
    }
    return result;
}

在Java 8中Comparator接口提供了一系列比较器方法,可使比较器流畅地构建。 这些比较器能够用来实现compareTo方法,就像Comparable接口所要求的那样。 许多程序员更喜欢这种方法的简洁性,尽管它的性能并不出众:在个人机器上排序PhoneNumber实例的数组速度慢了大约10%。 在使用这种方法时,考虑使用Java的静态导入,以即可以经过其简单名称来引用比较器静态方法,以使其清晰简洁。 如下是PhoneNumbercompareTo方法的使用方法:

// 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](http://COMPARATOR.compare(this), pn);
}

此实如今类初始化时构建比较器,使用两个比较器构建方法。第一个是comparingInt方法。它是一个静态方法,它使用一个键提取器函数式接口( key extractor function)做为参数,将对象引用映射为int类型的键,并返回一个根据该键排序的实例的比较器。在前面的示例中,comparingInt方法使用lambda表达式,它从PhoneNumber中提取区域代码,并返回一个Comparator<PhoneNumber>,根据它们的区域代码来排序电话号码。注意,lambda表达式显式指定了其输入参数的类型(PhoneNumber pn)。事实证实,在这种状况下,Java的类型推断功能不够强大,没法自行判断类型,所以咱们不得不帮助它以使程序编译。

若是两个电话号码实例具备相同的区号,则须要进一步细化比较,这正是第二个比较器构建方法,即thenComparingInt方法作的。 它是Comparator上的一个实例方法,接受一个int类型键提取器函数式接口( key extractor function)做为参数,并返回一个比较器,该比较器首先应用原始比较器,而后使用提取的键来打破链接。 你能够按照喜欢的方式屡次调用thenComparingInt方法,从而产生一个字典顺序。 在上面的例子中,咱们将两个调用叠加到thenComparingInt,产生一个排序,它的二级键是prefix,而其三级键是lineNum。 请注意,咱们没必要指定传递给thenComparingInt的任何一个调用的键提取器函数式接口的参数类型:Java的类型推断足够聪明,能够本身推断出参数的类型。

Comparator类具备完整的构建方法。对于longdouble基本类型,也有对应的相似于comparingIntthenComparingInt的方法,int版本的方法也能够应用于取值范围小于 int的类型上,如short类型,如PhoneNumber实例中所示。对于double版本的方法也能够用在float类型上。这提供了全部Java的基本数字类型的覆盖。

也有对象引用类型的比较器构建方法。静态方法comparing有两个重载方式。第一个方法使用键提取器函数式接口并按键的天然顺序。第二种方法是键提取器函数式接口和比较器,用于键的排序。thenComparing方法有三种重载。第一个重载只须要一个比较器,并使用它来提供一个二级排序。第二次重载只须要一个键提取器函数式接口,并使用键的天然顺序做为二级排序。最后的重载方法同时使用一个键提取器函数式接口和一个比较器来用在提取的键上。

有时,你可能会看到compareTocompare方法依赖于两个值之间的差值,若是第一个值小于第二个值,则为负;若是两个值相等则为零,若是第一个值大于,则为正值。这是一个例子:

// 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浮点运算失真的危险[JLS 15.20.1,15.21.1]。 此外,由此产生的方法不可能比使用上述技术编写的方法快得多。 使用静态compare方法:

**// 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的构建方法:

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

总而言之,不管什么时候实现具备合理排序的值类,你都应该让该类实现Comparable接口,以便在基于比较的集合中轻松对其实例进行排序,搜索和使用。 比较compareTo方法的实现中的字段值时,请避免使用"<"和">"运算符。 相反,使用包装类中的静态compare方法或Comparator接口中的构建方法。

相关文章
相关标签/搜索