Effective Java笔记第二章对所有对象都通用的方法
第五节考虑实现Comparable接口
1.compareTo方法并没有在Object中声明,相反,他是Comparable接口中唯一的方法。compareTo方法不但允许进行简单的等同性比较,而且允许执行顺序比较,除此之外他与Object的equals方法具有相似的特征,他还是个泛型。
2.一旦实现了Comparable接口,他就可以和许多泛型算法以及依赖于该接口的集合实现进行协作,付出很小的努力就可以获得非常强大的功能。事实上,Java平台类库中的所有值类都实现了Comparable接口。如果你正在编写一个值类(类具有自己特有的"逻辑相等"概念,值类仅仅是一个表示值的类,如Integer或者Date),它具有非常明显的内在排序关系,比如字母排序,按数值顺序或者年代顺序,那你就应该解决考虑实现Comparable这个接口:
public interface Comparable<T> {
public int compareTo(T o);
}
3.compareTo方法的通用约定与equals方法的相似:
将这个对象与指定的对象进行比较,当该对象小于,等于或者大于指定对象的时候,分别返回一个负整数,零和正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException异常。
4.在下面的说明中,符号sgn(表达式)表示数学中的signum函数,他根据表达式的值为负值,零和正值,分别返回-1,0和1。
1)实现者必须确保所有的x和y都满足sgn(x.compareTo(y))==-sgn(y.compareTo(x)),这实际也暗示着,当且仅当x.compareTo(y)抛出异常时, y.compareTo(x)也必须抛出异常。
2)实现者还必须确保这个关系是可传递的:x.compareTo(y)>0 && y.compareTo(z)>0暗示着x.compareTo(z) >0。
3)最后,实现者必须确保x.compareTo(y)暗示着所有的z都满足sgn(x.compareTo(z))==sgn(y.compareTo(z))。
4)强烈建议(x.compareTo(y)0)(x.equals(y)),但是这并非绝对必要,一般来说任何实现了Comparable接口的类,若违反了这个条件,都应该明确的予以说明。推荐使用这样的说法"注意:该类具有内在的排序功能,但是与equals不一致".。
5.在跨越不同类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同类的对象,compareTo可以抛出ClassCastException异常。
6.违反compareTo约定的类也会破坏其他依赖于比较关系的类,依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Coolections和Arrays,他们内部包含有搜索和排序算法。
7.如果遵守(x.compareTo(y)0)(x.equals(y)),那么由compareTo方法所施加的顺序关系就被认为"与equals一致"。如果违反了这个规则,顺序关系就被认为"与equals不一致",他仍然能够正常工作,但是如果一个有序集合包含了该类的元素,这个集合就可能无法遵守相应集合接口(Collection,Set,Map)的通用约定。这是因为,对于这些接口的通用约定是按照equals方法来定义的,但是有序集合使用了由compareTo方法而不是equals方法所施加的等同性测试。
8.CompareTo方法中的域的比较是顺序的比较,而不是等同性的比较。比较对象引用域可以是通过递归的调用compareTo方法来实现。如果一个域并没有实现Comparable接口,或者你需要使用一个非标准的排序关系,就可以使用一个显式的Comparable接口,或者编写自己的Comparator,或者使用已有的Comparator。下面我们举个例子:
public class ComparatorDemo {
private final String s;
public ComparatorDemo(String s) {
this.s = s;
}
public int compareTo(ComparatorDemo cd) {
int result = String.CASE_INSENSITIVE_ORDER.compare(s, cd.s);
return result;
}
public static void main(String[] args) {
ComparatorDemo cd1 = new ComparatorDemo("a");
ComparatorDemo cd2 = new ComparatorDemo("b");
System.out.println(cd1.compareTo(cd2));
}
}
9.比较整数型基本类型的域,可以使用关系操作符 < 和 > 。例如:浮点域用Bouble.compare或者Float.compare,而不用关系操作符,当应用到浮点值时,他们没有遵守compareTo的通用约定。对于数组域,则要把这些指导原则应用到每个元素上。
10.如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。你必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果(零代表相等),则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则进一步比较次关键的域,以此类推。如果所有的域都是相等的,则对象就是相等的,并返回零。下面我们举个例子:
public class PhoneNumber implements Comparable<PhoneNumber>{
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line Number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
//设置数字的范围
private static void rangeCheck(int arg, int max, String name) {
if (arg < 0 || arg > max) {
throw new IllegalArgumentException(name + ": " + arg);
}
}
//覆写equals方法
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof PhoneNumber)) {
return false;
}
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNumber == lineNumber && pn.areaCode == areaCode && pn.prefix == prefix;
}
@Override
public int hashCode() {
int result=17;
result=31*result+areaCode;
result=31*result+prefix;
result=31*result+lineNumber;
return result;
}
/**
* 返回此电话号码的字符串表示形式。
* 字符串由14个字符组成,格式为"(XXX) YYY-ZZZZ",其中XXX为区号,YYY为前缀,ZZZZ为行号(每个大写字母代表一个十进制数字)
*
* 如果有一些行号太短比如说是三位数,那么我们在开头用0来填充。例如,如果行号的值是123,则字符串表示形式的最后四个字符将是"0123"。
*
* 注意,区号后的右括号与前缀的第一个数字之间用一个空格隔开。
* @return
*/
@Override
public String toString() {
return String.format("(%03d) %03d-%04d",areaCode,prefix,lineNumber);
}
@Override
public int compareTo(PhoneNumber pn) {
//判断areaCode
if(areaCode<pn.areaCode){
return -1;
}
if(areaCode>pn.areaCode){
return 1;
}
//areaCode相同时判断prefix
if(prefix<pn.prefix){
return -1;
}
if(prefix>pn.prefix){
return 1;
}
//prefix相同时判断lineNumber
if(lineNumber<pn.lineNumber){
return -1;
}
if(lineNumber>pn.lineNumber){
return 1;
}
return 0;
}
public static void main(String[] args) {
PhoneNumber pn1 = new PhoneNumber(707, 867, 5309);
PhoneNumber pn2 = new PhoneNumber(701, 867, 5309);
System.out.println(pn1.compareTo(pn2));
}
}
compareTo方法的约定并没有指定返回值的大小,而只是指定了返回值的符号。你可以利用这一点来简化代码,或许可以提高他的运行性速度。
@Override
public int compareTo(PhoneNumber pn) {
//判断areaCode
int areaCodeDiff = areaCode - pn.areaCode;
if(areaCodeDiff!=0){
return areaCodeDiff;
}
//areaCode相同时判断prefix
int prefixDiff = prefix - pn.prefix;
if(prefixDiff!=0){
return prefixDiff;
}
//prefix相同时判断lineNumber
int lineNumberDiff = lineNumber - pn.lineNumber;
if(lineNumberDiff!=0){
return lineNumberDiff;
}
return 0;
}
这种方法用起来要非常小心,除非你确信相关的域不会为负值,或者更一般的情况:最小和最大的可能域值之差小于或等于INTEGER.MAX_VALUE(2³¹-1),否则就不要使用这种方法。这种技巧有时不能正常工作的原因在于,一个有符号的32位的整数还没有大到足以表达任意两个32位的整数的差。如果i是一个很大的正整数(int类型),而j是一个很大的负整数(int类型),那么(i-j)将会溢出,并返回一个负值。这样就使得compareTo方法将对某些参数返回错误的结果,违反了compareTo约定的第一条和第二条。
11.我们无法在使用新的值组件扩展可实例化的类时,同时保持compareTo约定,除非愿意放弃面向对象的抽象优势。针对equals的权宜之计适用于compareTo方法。如果你想为一个实现了Comparable接口的类增加值组件,请不要扩展这个类,而是要编写一个不相关的类,其中包含第一个类的一个实例。然后提供一个"视图"方法返回这个实例。这样既可以自由的在第二个类上实现compareTo方法,同时允许他的客户端在必要的时候,把第二个类的实例视为第一个类的实例。
原类
public class ComparableDemo implements Comparable<ComparableDemo> {
private final int x;
private final int y;
public ComparableDemo(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int compareTo(ComparableDemo cd) {
if (x < cd.x) {
return -1;
}
if (x > cd.x) {
return 1;
}
if (y < cd.y) {
return -1;
}
if (y > cd.y) {
return 1;
}
return 0;
}
public static void main(String[] args) {
ComparableDemo cd=new ComparableDemo(1,2);
ComparableDemo cd1=new ComparableDemo(12,2);
System.out.println(cd.compareTo(cd1));
}
}
扩展类
public class ComparableDemoSon {
private final ComparableDemo cd;
private final int z;
public ComparableDemoSon(int x, int y, int z) {
cd = new ComparableDemo(x, y);
this.z = z;
}
//创建公有视图
public ComparableDemo returnComparableDemo() {
return cd;
}
/**
* 先调用ComparableDemo类的compareTo方法,如果返回0的话,才会继续往下走,判断扩展的z是否相同。
* @param cs
* @return
*/
public int compareTo(ComparableDemoSon cs) {
int i = cd.compareTo(cs.cd);
if (i != 0) {
return i;
}
if (z < cs.z) {
return -1;
}
if (z > cs.z) {
return 1;
}
return 0;
}
public static void main(String[] args) {
ComparableDemoSon cs = new ComparableDemoSon(1, 2, 3);
ComparableDemoSon cs1 = new ComparableDemoSon(12, 2, 3);
System.out.println(cs.compareTo(cs1));
}
}