这篇博文主要介绍覆盖Object中的方法要注意的事项以及Comparable.compareTo()方法。
一、谨慎覆盖equals()方法
其实平时很少要用到覆盖equals方法的情况,没有什么特殊情况最好是使用原有提供的equlas方法。因为覆盖equals()方法时要遵循一些通用的约定之外,在与hash相关的集合类使用时,就必须要覆盖hashCode()方法了(第二点会强调)。
我们先说说覆盖equlas()方法要遵循哪些通用约定:
1、自反性:对于任何非null的引用值x, x.equals(x)必须返回true;
2、对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true;
3、传递性:对于任何非null的引用值x、y和z,当且仅当x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true;
4、一致性:对于任何一个非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)的结果依然一致。;
5、非空性:对于任何非null的引用值,x.equals(null)必须返回false。 且x必须不能为空,否则会抛出空指针异常。
其实上面这些约定看起来都看简单但也不能大意,很有可能你只满足了其中一点或者几点没有满足全部。如果没有满足全部的话,那么你覆盖equals()方法是不合理的,总会有那么中情况会超乎你的意料之外。
下面我们举例说明,其中自反性是很难违反的(我也想象不出怎么举反例测试,如有高手明白,请举例评论下),下面我举例验证对称性:
1 package test.effective; 2 /** 3 * @Description: 普通坐标类 4 * @author yuanfy 5 * @date 2017年7月13日 上午10:30:06 6 */ 7 class Point { 8 private int x; 9 private int y; 10 11 public Point(int x, int y) { 12 this.x = x; 13 this.y = y; 14 } 15 16 @Override 17 public boolean equals(Object obj) { 18 if (!(obj instanceof Point)) { 19 return false; 20 } 21 Point p = (Point) obj; 22 return this.x == p.x && this.y == p.y; 23 } 24 } 25 /** 26 * @Description: 带有颜色的坐标 27 * @author yuanfy 28 * @date 2017年7月13日 上午10:35:19 29 */ 30 class ColorPoint extends Point { 31 32 private String color; 33 34 public ColorPoint(int x, int y, String color) { 35 super(x, y); 36 this.color = color; 37 } 38 /** 39 * 与point不满足对称性示范案例 40 */ 41 @Override 42 public boolean equals(Object obj) { 43 if (!(obj instanceof ColorPoint)) { 44 return false;//与point实例比较直接返回false 45 } 46 ColorPoint p = (ColorPoint) obj; 47 return super.equals(p) && this.color.equals(p.color); 48 } 49 } 50 51 public class EqualsTest { 52 public static void main(String[] args) { 53 Point p = new Point(1,2); 54 ColorPoint cp = new ColorPoint(1, 2, "red"); 55 56 System.out.println(p.equals(cp));//输出结果:true 57 System.out.println(cp.equals(p));//输出结果:false 58 } 59 }
从上面的例子可以看出是违反了对称性规定的。问题原因在于:在比较普通点和有色点时,忽略了颜色的比较,而有色点跟普通点比较时,普通点不属于ColorPoint的实例,就直接返回了false。所以ColorPoint类中覆盖equlas()方法是有问题的,修改equals()方法后的代码如下:
1 @Override 2 public boolean equals(Object obj) { 3 //不属于Point实例对象 4 if (!(obj instanceof Point)) { 5 return false; 6 } 7 //不是ColorPoint实例对象,可能是Point实例对象或者其他类型对象 8 if (!(obj instanceof ColorPoint)) { 9 return super.equals(obj); 10 } 11 //ColorPoint实例对象 12 ColorPoint p = (ColorPoint) obj; 13 return super.equals(p) && this.color.equals(p.color); 14 }
测试代码如下:
1 ColorPoint cp1 = new ColorPoint(1, 2, "red"); 2 Point p = new Point(1,2); 3 ColorPoint cp2 = new ColorPoint(1, 2, "blue"); 4 5 System.out.println(cp1.equals(p));//输出结果:true 6 System.out.println(p.equals(cp1));//输出结果:true 7 8 System.out.println(p.equals(cp2));//输出结果:true 9 System.out.println(cp2.equals(p));//输出结果:true 10 11 System.out.println(cp1.equals(p));//输出结果:true 12 System.out.println(p.equals(cp2));//输出结果:true 13 System.out.println(cp1.equals(cp2));//输出结果:false
从修改后的例子中可以看出,这种方法确实满足了对称性,但是却不满足传递性。其实我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象带来的优势。当然我们可以不扩展Point的,所谓“复合优先于继承”。在ColorPoint假如一个私有的Point域,代码如下:
1 class ColorPoint{ 2 3 private String color; 4 5 private Point point; 6 7 public ColorPoint(int x, int y, String color) { 8 point = new Point(x, y); 9 this.color = color; 10 } 11 12 public Point asPoint(){ 13 return point; 14 } 15 16 @Override 17 public boolean equals(Object obj) { 18 if (!(obj instanceof ColorPoint)) {//满足非空性验证 19 return false; 20 } 21 //ColorPoint实例对象 22 ColorPoint p = (ColorPoint) obj; 23 return this.point.equals(p.point) && this.color.equals(p.color); 24 } 25 } 26 27 public class EqualsTest1 { 28 public static void main(String[] args) { 29 ColorPoint cp1 = new ColorPoint(1, 2, "red"); 30 Point p = new Point(1,2); 31 ColorPoint cp2 = new ColorPoint(1, 2, "red"); 32 ColorPoint cp3 = new ColorPoint(1, 2, "red"); 33 34 System.out.println(cp1.equals(p));//输出结果:false 35 System.out.println(p.equals(cp1));//输出结果:false 36 37 System.out.println(cp1.equals(cp2));//输出结果:true 38 System.out.println(cp2.equals(cp1));//输出结果:true 39 40 System.out.println(cp1.equals(cp2));//输出结果:true 41 System.out.println(cp2.equals(cp3));//输出结果:true 42 System.out.println(cp1.equals(cp3));//输出结果:true 43 } 44 }
上面的例子就满足对称性、传递性同时满足非空性。
接下来验证一致性:如果两个对象相等,它们就必须始终保持相等,除非他们中有一个对象或者两个都被修改了。换句话说,可变的对象在不同的时候可以与不同的对象相等, 而不可变对象则不会这样。比如说时间对象一个指定了时间,另外一个对象没有指定时间。当在某一个刻时候,他们既满足对称性和传递性,但是它不满足一致性。因为没有指定时间的对象的时间是一直在改变的。
所以当你编写完成了equlas之后,应该测试验证是否满足这个几个特性。
二、覆盖equals时总要覆盖hashCode
书中提出:在每个覆盖了equlas方法的类,也必须覆盖hashCode方法。如果不这样做的话, 就会违反Object.hashCode()的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和HashTable。
其中约定简洁如下(详细请参考书中):
1、相等的对象必须具有相等的散列码(hash code)
2、不相等的对象未必是不一样的散列码。也就是说相同散列码的两个对象,两个对象未必相等。但是相等的两个对象,一定就有相等的散列码。
下面根据例子来说明:
1 package test.effective; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 public class PhoneNumber { 7 private int areaCode; 8 9 private int prefix; 10 11 private int lineNumer; 12 13 public PhoneNumber(int areaCode, int prefix, int lineNumer) { 14 this.areaCode = areaCode; 15 this.prefix = prefix; 16 this.lineNumer = lineNumer; 17 } 18 19 @Override 20 public boolean equals(Object obj) { 21 if (obj == this) { 22 return true; 23 } 24 if (!(obj instanceof PhoneNumber)) { 25 return false; 26 } 27 PhoneNumber pn = (PhoneNumber) obj; 28 return this.areaCode == pn.areaCode 29 && this.prefix == pn.prefix 30 && this.lineNumer == this.lineNumer; 31 } 32 33 public static void main(String[] args) { 34 35 Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>(); 36 PhoneNumber pn1 = new PhoneNumber(408, 867, 5309); 37 PhoneNumber pn2 = new PhoneNumber(408, 867, 5309); 38 39 System.out.println(pn1.equals(pn2));//输出结果:true 40 41 map.put(pn1, "Jany"); 42 43 System.out.println(map.get(pn1)); 44 System.out.println(map.get(pn2)); 45 } 46 }
我们将覆盖了equals方法的类结合基于散列码的集合使用。从上面例子39行知道pn1实例和pn2实例是相等,行43代码输出的结果可想而知是Jany,但是行44输出什么呢,估计没有仔细考虑的话,可能第一感觉也会输出null。我们看下HashMap中get方法源码就知道了。
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key);//获取对象的hash值 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; //hash必须相等才有可能进行返回 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
从源码中得知get()方法是要根据hash值获取的。然后我们在看看pn1和pn2对象的hash码。
System.out.println(pn1.hashCode());//2007692877 System.out.println(pn2.hashCode());//2031122075
所以显而易见见,上述map.get(pn2) 的返回值是为null的。
修正这个问题很简单,在PhoneNumber类中覆盖hashCode()方法即可,先提供简单的案例:
@Override public int hashCode() { return 1; }
这样的能满足上面那个例子map.get(pn2) 返回“Jany”,但是这样 所有实例都是返回一样的hashCode是不可取的。参考书中最正确的方式:
@Override public int hashCode() { int result = 17; result = 31 * result + areaCode; result = 31 * result + prefix; result = 31 * result + lineNumer; return 1; }