今天在写程序的时候遇到一个问题,问题的简化描述如下:
我们设计了一个类Point用来存储地名和相应的经纬度,相关代码如下:
1 import java.util.HashSet; 2 import java.util.Set; 3 4 public class Point { 5 private String addr; 6 private int latitude; 7 private int longitude; 8 9 public Point(String a, int la, int lo) { 10 this.addr = new String(a); 11 this.latitude = la; 12 this.longitude = lo; 13 } 14 15 @Override 16 public boolean equals(Object obj) { 17 if (!(obj instanceof Point)) { 18 return false; 19 } else { 20 if (obj == this) { 21 return true; 22 } else { 23 Point p = (Point) obj; 24 return addr.equals(p.addr) && latitude == p.latitude && longitude == p.longitude; 25 } 26 } 27 } 28 29 @Override 30 public String toString() { 31 return new String(addr + ":(" + latitude + "," + longitude + ")"); 32 } 33 34 public static void main(String[] args) { 35 Point p1 = new Point("Beijing", 40, 116); 36 Point p2 = new Point("Beijing", 40, 116); 37 Set<Point> hashset = new HashSet<>(); 38 hashset.add(p1); 39 hashset.add(p2); 40 System.out.println(hashset); 41 System.out.println(hashset.contains(new Point("Beijing", 40, 116))); 42 } 43 }
这时候,程序的输出是:
1 [Beijing:(40,116), Beijing:(40,116)] 2 false
这个结果显然与Set的不含重复元素的属性不符合,并且调用Set的contains得到的结果也与我们期望的完全不符。参考HashSet的java docs,里面提到了:public boolean contains(Object o) Returns true if this set contains the specified element.More formally, returns true if and only if this set contains an element e such that (o == null ? e == null : o.equals(e))。此外,关于add方法,java docs中的描述也是对于一个待添加的元素E e, 当集合中找不到一个元素e2使得(e == null ? e2 == null : e2.equals(e))时才将e添加到集合中并返回true,否则原集合保持不变并返回false。因此,HashSet中用来判断两个元素的方法是调用描述元素e的类的equals方法,即比较的是元素的值而不是元素的引用地址。但在上面的代码中,我们已经重写了Point的equals方法,使得判断两个Point实例是否相等是根据Point封装的内部的值来判断的,但是为什么程序还是给出了错误的结果呢?
为了了解HashSet的实现的原理,我查看了jdk中HashSet的源码,HashSet实际上是由HashMap实现的,所以查看HashMap的源码。以HashMap<K,V>的containskey(Object o)方法为例,首先要通过hash方法计算出K key的hash值,然后通过这个hash值定位到存储这个键值的位置,然后比较这个位置上存储的K元素是否与key相等(通过equals方法)。因此,Set的contains方法(调用的其实就是HashMap的containsKey方法)并不是把集合中所有的元素都与待比较元素都用equals方法比较一遍,而是只对hash值定位到的元素调用equals方法。hash值的计算需要调用对象的hashCode方法。在上面的代码中,我们并没有对hashCode进行重写,因此有可能造成值相同的元素却对应不同的hash值。
此外,上面的程序在设计时没有考虑到equals()方法和hashCode()方法之间的设计实现原则。关于equals()方法和hashCode()方法的紧密联系,以下参考了Java中Set的contains方法这篇文章(点击链接可阅读原文)。这两个方法的设计实现原则为:
如果两个对象相等(使用equals()方法),那么必须拥有相同的哈希码(使用hashCode()方法).
即使两个对象有相同的哈希值(hash code),他们不一定相等.意思就是: 多个不同的对象,可以返回同一个hash值.
因此,我们在重写equals()方法时,最好将hashCode()也进行重写。上面的代码加上以下代码后,就能得到期望的结果。
1 @Override 2 public int hashCode() { 3 String s = new String(addr + latitude + longitude); 4 return s.hashCode(); 5 }