每个覆盖equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反 Object.hashCode
的通用约定,这个约定的内容如下:
摘自Object规范[JavaSE6]:
equals(Object) equals(Object)
因此如果覆盖equals方法时没有覆盖hashCode方法,就相当于违反了第二条约定:相等的对象必须具有相等的散列码
具体地,根据类的equals方法,两个截然不同的实例在逻辑上有可能是相等的,但是,根据Object类的hashCode方法,它们仅仅是两个没有任何共同之处的对象,所以如果不覆盖hashCode的话,这两个在逻辑上相等的对象的hashCode返回的结果是两个随机的整数,而不是第二条约定那样返回两个相等的整数。
说的可能繁琐,不如举个例子来看看。
假设我们有一个矩形类 Rectangle
,它的定义如下:
public class Rectangle{ private int width; private int height; public Rectangle(int width,int height){ this.width = width; this.height = height; } }
那么从逻辑上说,如果两个矩形的宽和高是一样的,那么我们就认为这两个矩形是相同的。但是对对象而言,即使两个矩形对象的宽和高一样,但是由于这两个并不是同一个对象,所以进行比较的话结果是false。
Rectangle rectangle1 = new Rectangle(3, 4); Rectangle rectangle2 = new Rectangle(3, 4); System.out.println(rectangle1 == rectangle2); //false System.out.println(rectangle1.equals(rectangle2); //false
所以,首先要实现只要宽和高相同就是同一个矩形这种逻辑,我们需要覆盖其equals方法:
@Override public boolean equals(Object obj) { Rectangle o = (Rectangle) obj; if (length == o.length){ if (width == o.width){ return true; }else return false; } else if (width == o.length) { if (length == o.width) { return true; }else return false; } return false; }
System.out.println(rectangle1.equals(rectangle2); //true
那么为什么要覆盖hashCode呢?
主要是因为如果不继续覆盖hashCode的话,那么会导致 Rectangle
类无法跟所有基于散列(hash)的集合一起使用,包括HashMap、HashSet和HashTable等。
如果不覆盖hashCode使用HashMap时,我们可以测试一下:
HashMap<Rectangle, String> map2 = new HashMap<>(16); map2.put(rectangle1, "nice"); map2.put(rectangle2, "bad"); System.out.println("size = " + map2.size()); // 2
这时候的输出结果是2,但是我们已经认为 rectangle1
和 rectangle2
在逻辑上是相等的了,根据HashMap的特性, rectangle1
和 rectangle2
应该是同一个key值,也就是说, rectangle2
应该会覆盖掉 rectangle1
的value值,即 bad
应该覆盖了 nice
,最终结果应该是 size = 1
,但事实却并不是像我们想象的那样。这是因为 put
操作是通过hashCode计算得出对象的散列值,由于没有覆盖hashCode方法,导致这两个对象的hashcode值像两个随机数,因此大概率会分配到不同的散列桶中(不知道这个概念可以看一下HashMap原理),而即使这两个对象恰好被放到同一个散列桶中,底层还是会认为这两个不是同一个对象,这是因为HashMap有一个优化,可以将每个项相关联的散列码(hashcode)缓存起来,如果散列码不匹配,也不必再去检验对象的等同性。
想要让 rectangle1
和 rectangle2
做为同一个key,在散列时落在同一个桶中,我们就必须要来覆盖hashCode方法。一个好的散列函数通常倾向于为不相等的对象产生不相等的散列码,这也正是上述第三条约定的含义。通常我们覆盖hashCode的计算方法如下:
@Override public int hashCode() { int result = 17; result = 31 * result + length*width; return result; }
上面的值17是任选的,而之所以选择31,一方面是因为它是一个奇素数,因为如果乘数是偶数,并且乘法溢出的话,信息就会丢失;另外一方面31有一个很好的特性,就是可以用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5) - i
,现代的VM都可以自动完成这种优化。
在我们覆盖hashCode后,再试一次调用:
HashMap<Rectangle, String> map2 = new HashMap<>(16); map2.put(rectangle1, "nice"); map2.put(rectangle2, "nice"); System.out.println("size =" + map2.size()); //1
这样就可以得到我们想要的结果了!