m88体育 前一阵子准备雅思考试,没时间继续Effective Java,现在考完了,要捡起来。
实现Comparable接口
m88体育 Comparable接口只有唯一一个方法:
public interface Comparable<T> { int compareTo(T t); }
compareTo方法的返回值是一个整数。对于a.compareTo(b),如果返回负值,则表明a < b;正值表明a > b;如果返回0,则说明a与b相等。如果a与b类型不同,应该弹出ClassCastException。
所有实现了此接口的类的对象都可以被"自然排序",查找、极值、维序等操作也会变得非常简单。比如排序一个这样的对象构成的数组:
Arrays.sort(a);
实现Comparable接口时,要遵守的合约很类似于Object类的equals方法:
1. 对称:如果a > b,那么必须有b < a
2. 传递:如果a > b,而且b > c,那么必须有a > c
3. 一致:如果a = b,那么对任何c,且a > c,则必然有b > c,反之亦然。
4. 如果用compareTo方法判定两个对象相等,那么用equals方法判定,最好二者也是相等的。这不是一条强制性和约,但它是被强烈建议遵守的。反之,如果这一条不能被遵守,则应在注释和文档中明确指出:
This class has a natural ordering that is inconsistent with equals.
针对上述第4条,有一个有趣的例子:BigDecimal类,它是不遵守第4条合约的。为了看出这个violation的影响,可以建立两个对象,BigDecimal("1.0")和BigDecimal("1.00"),并试着分别把它们加入HashSet和TreeSet:
BigDecimal firstObj = new BigDecimal("1.0"); BigDecimal secondObj = new BigDecimal("1.00"); HashSet<BigDecimal> hashSet = new HashSet<BigDecimal>(); hashSet.add(firstObj); hashSet.add(secondObj); System.out.println("Size of HashSet: " + hashSet.size()); TreeSet<BigDecimal> treeSet = new TreeSet<BigDecimal>(); treeSet.add(firstObj); treeSet.add(secondObj); System.out.println("Size of TreeSet: " + treeSet.size());
执行上面的代码,输出是:
Size of HashSet: 2 Size of TreeSet: 1
这是因为,对于普通的Collection类,比如HashSet,元素的相等性是由equals方法判定,而BigDecimal("1.0").equals(BigDecimal("1.00"))是返回false的,所以对于HashSet来说,它们是两个不同的元素。而对于有序的Collection,比如TreeSet,由compareTo方法判定相等性,并且BigDecimal("1.0").compareTo(BigDecimal("1.00"))返回0,所以对于TreeSet来说二者是同一个元素,只有一个被加入Set中。由此例可以看出,第4条合约虽然不是强制合约,但如果被破坏,将严重影响程序的行为。
compareTo方法的编码,是逐个比较两个对象的属性,按重要性由高至低进行比较:
- 对于整数类型的数据,用>和<运算符
- 对于浮点类型的数据,用Double.compare或者Float.compare方法
- 对于对象类型,则递归调用属性的compareTo方法,或者进行业务逻辑比较
- 对于数组类型,则逐个比较其元素
一旦遇到可以决定大小的属性,则立即返回结果。如果所有属性都相等,则返回0。下面是item 9中电话号码的compareTo方法:
public int compareTo(PhoneNumber pn) { // Compare area codes if (areaCode < pn.areaCode) return -1; if (areaCode > pn.areaCode) return 1; // Area codes are equal, compare prefixes if (prefix < pn.prefix) return -1; if (prefix > pn.prefix) return 1; // Area codes and prefixes are equal, compare line numbers if (lineNumber < pn.lineNumber) return -1; if (lineNumber > pn.lineNumber) return 1; return 0; // All fields are equal }
因为compareTo的返回值中,数值并不重要,我们只需要知道其是正值还是负值或者零,所以上面的代码可以有如下优化:
public int compareTo(PhoneNumber pn) { // Compare area codes int areaCodeDiff = areaCode - pn.areaCode; if (areaCodeDiff != 0) return areaCodeDiff; // Area codes are equal, compare prefixes int prefixDiff = prefix - pn.prefix; if (prefixDiff != 0) return prefixDiff; // Area codes and prefixes are equal, compare line numbers return lineNumber - pn.lineNumber; }
做这样的优化需要倍加小心,要对各属性的数值范围有所了解,以避免相减操作结果的超界。