compareTo方法并没有在Object中声明。相反,它是Comparable接口中唯一的一个方法。compareTo方法不但允许进行简单的同等性比较,而且允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特性,还是个涉及到泛型的方法。类实现了Comparable接口,就表明它的实例具有内在的排序关系(natural ordering)。给实现Comparable接口的对象数组进行排序,只需要下面这一行代码:
Arrays.sort(a);
对于存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也是同样的简单。例如下面的程序依赖于String实现了Comparable接口,它去掉了命令行参数列表中的重复参数,并按字母表顺序打印出来了:
public class WordList {
public static void main(String[] args) {
Set s = new TreeSet();
Collection.addAll(s, args);
System.out.println(s);
}
}
一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。你付出了很小的努力就可以获得非常强大的功能。事实上,Java平台类库中的所有值类都实现了Comparable接口。如果我们需要写一个类时,当这个类有非常明显的内在排序关系,我们就应该优先考虑实现这个接口:
public interface Comparable {
int compareTo(T t);
}
Comparable接口的规范
compareTo方法的通用约定和equals方法的很相似,将一个对象与指定对象进行比较。当该对象小于、等于或者大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法和该对象进行比较,则抛出ClassCastException异常。
在下面的说明中,符号sgn(表达式)表示数学中的signum函数,它根据表达式的值为负值、零和正值,分别返回-1、0、1。
确保所有的属性都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。当y.compareTo(x)抛出异常时,x.compareTo(y)也要抛出异常。这条规则和equls规范里面的对称性类似。
必须确保比较关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)同时x.compareTo(z) > 0也成立。对应着equals使用规范里面的传递性。
必须确保x.compareTo(y) == 0同时所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但是并不是绝对必要的。如果一个类实现了Comparable接口,并且违反了这个条件,我们应该明确予以说明。推荐使用这样的说法:“注意,该类具有内在的排序功能,但是与equals不一致”。
在类的内部,任何合理的顺序关系都可以满足compareTo的约定。在跨越不同类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同的对象,compareTo可以抛出ClassCastException异常。通常,这正是compareTo在这种情况下应该做的事情,如果类设置了确定的参数,这也正式它要做的事情。虽然以上约定没有把跨类之间的比较排除在外,但是从Java1.6发行版本开始,Java平台类库中就没有支持跨类比较的这种特性了。
就好像违反了hashCode约定的类会破坏其他依赖于散列算法的做法的情况一样,违反了compareTo约定的类也会破坏其他依赖于比较关系的类。依赖于比较关系的类包括有序集合类TreeSet和TreeMap、以及工具类Collections和Arrays,它们内部包含有搜索和算法排序。
上面的三个条款的一个直接结果就是,有compareTo方法施加的同等性测试,也一定遵守相同于equals约定所施加的限制条件:自反性、对称性和传递性。因此,下面的告诫也同样的适用:无法在用新的值组件扩展可实例化的类时,同时保持compareTo约定,除非愿意放弃面对对象的抽象优势。如果你想为一个实现了Comparable接口的类增加值组件,不扩展这个类;要便携一个不相关的类,其中包含第一个类的一个实例。然后提供一个“视图(view)”方法返回这个实例。这样既可以让你自由地在第二个类上实现compareTo方法,同时也允许它的客户端在必要的时候,把第二个类的实例看作第一个类的实例。运用组合优于继承。
compareTo约定的最后一段是一个强烈的建议,而不是真正的规则,只是说明了compareTo方法施加的同等性测试,在通常情况下就应该返回与equals方法同样的结果。如果遵守了这一条规定,那么由compareTo方法所施加的关系顺序就会被认为”于equals一致”。如果违反了这条规则,则会相反。如果一个类的compareTo方法施加了一个与equals方法不一致的顺序关系,它仍然能够工作,但是如果有一个有序集合包含了该类的元素,这个集合就可能无法遵守相应集合接口(Collection、Set或Map)的通用约定。这是因为,对于这些接口的通用约定是按照equals方法来定义的,但是有序集合时使用由compareTo方法而不是equals方法所施加的同等性测试。
例如,考虑BigDecimal类,它的compareTo方法和equals方法不一致。如果你创建了一个HashSet实例,并且添加了new BigDecimal(“1.0”)和new BigDecimal(“1.00”),这个集合就将包含两个元素,因为新增到集合中的两个BigDecimal实例,通过equals方法来比较的时候是不相等的。然而如果你使用TreeSet而不是HashSet来执行同样的过程,集合中将只包含一个元素,因为这两个BigDecimal实例在通过compareTo方法进行比较的时候是相等的。
编写compareTo方法与编写equals方法非常相似,但也存在几处重大的差别。因为Compareable接口时参数化的,而且comparable方法是静态的类型,因此不必进行类型检查,也不需要对它的参数进行类型的转换。如果参数的类型不合适,这个调用甚至无法编译。如果参数为null,这个调用。如果参数为null,这个调用应该抛出NullPointerException异常,并且一旦该方法试图访问它的成员变量时就应该抛出。
CompareTo方法中的域的比较是顺序的比较,而不是同等性的比较。比较对象引用域可以是通过递归地调用compareTo方法来实现。如果一个域并没有实现Compareable接口,或者你需要使用一个非标准的排序关系,就可以使用一个显示的Comparator来代替。或者是编写自己的Comparator,或者是使用已有的Comparator,例如(第八条没有实现这个方法,此例比较方法是jdk中的方法)针对下面的这个类,已经有一个compareTo方法:
public final class CaseInsensitiveString implements Comparable {
public int comparaTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
... //Remainder omitted
}
比较整数型基本类型的域,可以使用关系操作符<和>。例如,浮点域用Double.compare或者Float.compare,而不同关系操作符,当应用到浮点值得时候,它们没有遵守compareTo的通用约定。对于数组域,则要把这些知道原则应用到每个元素上。
如果一个类有多个关键域,那么比较这些关键域的顺序非常关键。必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果(0代表着相等),则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则再比较下一个关键域,以此类推,如果所有域都是相等的,那么才返回0。例如下面的例子:
public final class PhoneNumber implements Comparable {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix,
int lineNumber) {
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
@Override
public int compareTo(PhoneNumber pn) {
if (areaCode < pn.areaCode)
return -1;
if(areaCode > pn.areaCode)
return 1;
if (prefix < pn.prefix)
return -1;
if (prefix > pn.prefix)
return 1;
if (lineNumber < pn.lineNumber)
return -1;
if (lineNumber > pn.lineNumber)
return 1;
return 0;
}
}
虽然这个方法可行,但它还可以进行改进。先来看看compareTo方法的约定,并没有指定返回值的大小(magnitude),而只是指定了返回值的符号。可以利用这一点来简化代码,或许还可以提高它的运行速度:
public int compareTo(PhoneNumber pn) {
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff != 0)
return areaCodeDiff;
int prefixDiff = prefix - pn.prefix;
if (0 != prefixDiff)
return prefixDiff;
return lineNumber - pn.lineNumber;
}
使用这种方法的时候需要注意,有符号的32位整数还不足以大到能够表达任意两个32位整数的差值,如果i是一个很大的正整数,j是一个很小的负整数,i-j有可能会溢出,并且返回一个负值= 。