12. Consider implementing Comparable
大意为 考虑实现Comparable接口
对于Comparable接口来说,其主要方法应该是compareTo方法,可是这个方法并没有在Object里面声明,而是Comparable接口中唯一的方法,这个方法所能够产生的作用并不局限于简单的比较,还可以是有顺序的比较
换句话说,实现Comparable接口的类,都具有一种内在的排序关系(natural ordering),而且对该类的数组进行排序也是比较简单的,例如:
Arrays.sort(a);
对于储存在集合里面的实现了Comparable接口的对象进行搜索,计算极值之类的操作都十分简单,下面举个例子,实现了从命令行读取参数,并且自动排序并打印
public class ComparableTest {
public static void main(String[] args){
Set<String> s=new TreeSet<String>();
Collections.addAll(s,args);
System.out.println(s);
}
}
我们在IntelliJ里面设定命令行参数,设定方法如下
主面板-> Run -> Edit Configurations
如上图所示,我们把参数设成 “apple cat interest home bed”这5个字符串
得到的结果为
我们可以在参数后面继续加一个apple,你会发现结果还是一样的,这说明此内在排序关系还能够剔除重复的元素
可以看出来,这个接口的功能还是十分强大的,Java中的一些值依赖的类都实现了这个接口,所以当你想要实现排序,不管是字母顺序排序还是数值排序,都应该考虑实现这个接口
Comparable接口中的compareTo方法其实与我们之前所介绍的equals类似,按照两个对象的比较大小返回相应的结果,分别是0,正整数,负整数,如果无法比较的话就抛出一个ClassCastException
在这里我们引入一个数学函数signum也就是sgn符号函数,该函数根据自变量的符号分别返回-1,0,+1
接下来,利用这个数学函数,我们介绍一下实现Comparable接口的一些约定
- 所有的x,y需满足,sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
- 传递性: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) ,虽然这个不是必要的,但是如果违反这一条件,应该做出适当说明
以上是我们必需遵守的规范,如果违反了这些规范,那么就会破坏了一些依赖于比较关系的类,比如说一些有序的集合类,TreeSet,TreeMap,或者是一些工具类,比如Collection和Arrays,它们的内部都有搜索和排序算法
再此需要提及的是,实现了Comparble接口的类,如果你想要为它添加一些值依赖的域,那么首要的选项必然不是去直接添加成员域来进行扩展,比较好的方法应该是重新写一个类,在这个新的类里面有着扩展前的类的实例,并且包含一个“View”来返回这个实例
对着上面的约定中的最后一项,并不是真正的规则,而是同equals返回的结果一致,保持一种一致的顺序关系,当然了,一个类违反了这个小规则,也可以继续正常工作,不过当一个有序的集合的元素违反了这个规则,那么这个集合可能就不能遵守一些集合,比如Collection,Set,Map等的通用的接口约定,这是因为这些通用的接口约定是利用equals定义的
在这里举一个例子,BigDecimal的这个类,它的compareTo方法和equals方法并不一致,如果你用HashMap创建一个集合,并且添加new BigDecimal(1.0)和new BigDecimal(1.00)这两个实例,这个集合将包含两个元素,用TreeMap来实现的话就只有一个元素存在,这是因为前者是利用equals,后者是利用compareTo,我们实际来测试一下
public class ComparableTest {
public static void main(String[] args){
TreeSet<BigDecimal> set1= new TreeSet<>();
HashSet<BigDecimal> set2=new HashSet<>();
set1.add(new BigDecimal(1.0));
set1.add(new BigDecimal(1.00));
set2.add(new BigDecimal(1.0));
set2.add(new BigDecimal(1.00));
System.out.println("Set1's size is "+set1.size());
System.out.println("Set2's size is "+set2.size());
System.out.println(new BigDecimal(1.00).compareTo(new BigDecimal(1.0)));
System.out.println(new BigDecimal(1.00).equals(new BigDecimal(1.0)));
}
}
结果如下图所示:
可以看出,书中所提的bug在1.8已经被修复了,但是BigDecimal的两个方法不一致的问题还是存在
编写compareTo方法和equals方法差不多,但是有几个重要的区别,Comparable接口是参数化的,并且comparable方法是静态的类型,那么我们就不用对参数进行类型转换,如果参数类型不合适的话,甚至无法通过编译
注意到compareTo方法其实是顺序的比较,比较对象的引用域的时候我们可以递归地使用compareTo方法来解决,如果一个域没有实现Comparable接口,或者我们需要一种另类的排序关系来比较的话,可以考虑使用Comparator,使用已有的或者是自己编写一个
需要提及的是,当一个类有多个关键域的时候,我们需要自己拟定比较的顺序,举一个例子,在笔记9中出现的PhoneNumber类
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
}
由于我们并不要求返回的值,只要求返回的符号,所以以上的代码可以简化成
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;
}
当然这样做要注意两者的差值不能超过int的表示范围,否则就溢出了