Java性能优化(11):考虑实现Comparable接口
一个类实现了Comprable接口,就表明它的实例具有内在的排序关系。若一个数组中的对象实现了Comparable接口,则对这个数组进行排序非常简单:
Arrays.sort(a);
对于存储在集合中Comarable对象,搜索计算极值以及自动维护工作都非常简单。例如,下面的程序依赖于String实现了Comparable接口,它去掉了命令行参数列表中的重复参数,并按字母表顺序打印出来:
public class WordList
{
public static void main(String[] args)
{
Set s = new TreeSet();
s.addAll(Arrays.asList(args));
System.out.println(s);
}
}
一旦你的类实现了Comparable接口,它就可以跟许多泛型算法,以及依赖于该接口集合实现进行协作。事实上,Java平台中所有值类都实现了Comparable。如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母表顺序、按数值顺序或者年代顺序,那么你总是应该考虑实现这个接口。
CompareTo方法的通用约定与equals方法的通用约定具有相似的特征,下面是它的内容。
将当前这个对象与指定对象进行顺序比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型使得无法进行比较,则抛出ClassCastException异常。
在下面的说明中,记号表示数学上的signum函数,它根据expresssion的值为负值、零和正值,分别返回-1、0或1.
实现者必须保证对于所有的x和y,满足sgn(x.compareTo(y))=-sgn(y.compareTo(x))。
实现者也必须保证这个比较关心是可传递的:(x.compareTo(y))>0&&y.compareTo(z)>0暗示着x.compareTo(z)>0。
最后,实现者保证x.compareTo(y)==0 暗示着:对于所有的z,sgn(x.comrpareTo(z))==sgn(y.compareTo(z))。
强烈建议(x.compareTo(y)==0)==(x.equals(y)),但这不是严格要求。一般而言,任何实现了Comparable接口的类,若违反了这个条件,应该明确予以说明。推荐使用这样的说法:”注意:该类具有内在排序功能,但是与equals不一致。”
请不要对上述约定中的数学关系感到厌烦。如同equals约定一样,compareTo约定并没有它看起来那么复杂。在一个类的内部,任何一种合理的顺序关系都可以满足compareTo约定。然而,与equals不同的是,在跨越不同类的时候,compareTo可以不做比较:如果两个被比较对象引用分别指向不同类的对象,那么compareTo可以抛出ClassCastException异常。通常,这正是compareTo在这种情况下应该做的事情。
就像一个违反了hashcode的约定的类会破坏其他的依赖于散列做法的类一样,一个违反了compareTo约定的类也会破坏其他依赖于比较关系的类。依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collection和Arrays,它们内部包含有搜索和排序算法。
域的本身是顺序比较,而不是相等比较。比较对象引用域可以通过递归地调用compareTo方法来实现。如果一个域斌没有实现Comparable接口,或者你需要使用一个域并没有实现Comparator。或者编写专门的Comparator。或者使用已有的Comparator,其compareTo方法使用一个已有的Comparator:
public int compareTo(Object o)
{
CaseInsensitiveString cis = (CaseInsensitiveString)o;
return String.CASE_INSENSITIVE_ODER.compare(s, cis.s);
}
比较原语类型的域,你可以使用关系操作符<和>;比较数组域是,你可以把这些指导原则应用到每一个元素上。如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。你必须从最关键的域开始,逐步进行到所有的重要域。如果有一个域的比较产生了非零的结果,则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则进一步比较次的最关键的域,以此类推。如果所有域都是相等的,则对象是相等的,返回零。
public int compareTo(Object o) {
PhoneNumber pn = (PhoneNumber) o;
if (areaCode < pn.areaCode)
return -1;
if (areaCode > pn.areaCode)
return 1;
if (exchange < pn.exchange)
return -1;
if (exchange > pn.exchange)
return 1;
if (extension < pn.extension)
return -1;
if (extension > pn.extension)
return 1;
return 0;
}
虽然这个可以工作的很好,但它还有改进的余地。回想一下,compareTo方法的约定并没有指定返回值的大小,而是指定了返回值的符号。你可以利用这一点来简化代码,提高运行速度。
public int compareTo(Object o) {
PhoneNumber pn = (PhoneNumber) o;
int areaCodeDiff=areaCode-pn.areaCode;
if(areaCodeDiff!=0)
return areaCodeDiff;
int exchangeDiff=exchange-pn.exchange;
if(exchangeDiff!=0)
return exchangeDiff;
return extension-pn.extension;
}
要谨慎使用这项技巧,除非你确信问题中的域不会是负的,或者更一般的情况,最小和最大的可能域值之差小于或者等于INTEGER.MAX_VALUE(2,-1),否则就不要使用这种方法。这项技巧往往不能正常工作的原因在于,一个有符号的32位整数还没有大到足以表达任意两个32位的差。如果i是一个很大的正整数,而j是一个绝对值很大的负整数,,那么(i-j)将会溢出,并返回一个负值。这就使得compareTo不能工作。对于某些实参,它会返回无意义的结果。