hashCode方法是Java的Object类所定义的几个基本方法之一。我们可以深入到Object类的源码中去查看:
public native int hashCode();
其中native关键字表明这个函数是由非java语言来实现的,这个函数的功能就是返回这个对象在内存中的地址。
大部分类都会重新覆写一下hashCode方法,原因有很多。那么这个方法主要被应用在什么场景下呢?一个非常重要的应用就是当我们处理散列集合类的时候,比如HashSet,HashMap,HashTable。
java之所以要设计这三种散列集合类,目的就是能够在常数时间复杂度O(1)下,完成“确定一个元素是否在一个集合中”,“插入元素”,“删除元素”等操作。采取的思路就是使用哈希表。这是一个我们经常见到的数据结构。我们在实现一个哈希表时最重要的一步操作就是通过这个元素的关键字来计算它的哈希值,从而就可以通过这个哈希值来查询哈希表,看看这个元素是否在集合中。那么hashCode方法的作用就是提供这个元素的关键字。
我们可以具体看一下HashMap里面几个重要方法的实现:
1 public V put(K key, V value) { 2 if (key == null) 3 return putForNullKey(value); 4 int hash = hash(key.hashCode()); 5 int i = indexFor(hash, table.length); 6 for (Entry<K,V> e = table[i]; e != null; e = e.next) { 7 Object k; 8 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 9 V oldValue = e.value; 10 e.value = value; 11 e.recordAccess(this); 12 return oldValue; 13 } 14 } 15 16 modCount++; 17 addEntry(hash, key, value, i); 18 return null; 19 }
这个方法是向集合中插入一个新的“键值对”元素,其中:
第4行首先通过hashCode计算这个元素的关键字。
第4行调用hash()函数,计算这个关键字的哈希值。
第5行计算这个哈希值对应于哈希表中的哪个位置,HashMap会维护一个哈希表 Entry[] table。每一个位置对应一个哈希值,并且任意一个Entry[i]元素其实是一个链表头,这个链表中存放着在集合中所有哈希值为i的元素。
第6~13行就要遍历这个table[i]所管理的链表,并且查询这个链表中是否已经存在了关键字为k的元素。如果查找到了,则用新的value去覆盖旧的value,并且函数会把旧的value返回。
第16~18行,如果运行到这里代表这个一个全新的元素,此时就把它加到集合中。
另外其他的方法比如get,也会运用到hashCode方法。
hashCode()方法在Object的实现中,是返回对象的内存地址,但是通常在第一个类的时候,我们经常会覆写hashCode()方法,比如在Integer类中,hashCode()就是返回这个Integer的值。
所以这就会造成多个问题,或者说多个结论:
1. 如果两个引用hashCode()返回的值相同,并不能代表这两个引用指向的是同一个对象。
2. 如果两个引用指向的是同一个对象,则它们的hashCode()方法一定返回一样的值。
3. 如果两个引用的hashCode值返回的是不一样的值,则这两个引用一定指向的不是同一个对象。
实例1:
1 public static void main(String[] args) { 2 Integer i1 = new Integer(2); 3 Integer i2 = new Integer(2); 4 System.out.println("i1 hashCode is "+i1.hashCode()+" i2 hashCode is "+i2.hashCode()); 5 if(i1.hashCode() == i2.hashCode()) 6 System.out.println("i1, i2 have same hashCode"); 7 else 8 System.out.println("i1, i2 have different hashCode"); 9 if(i1 == i2) 10 System.out.println("i1, i2 are same"); 11 else 12 System.out.println("i1, i2 are different"); 13 }
这个例子就是说明结论1的,i1,i2都是Integer对象,且值都是2,Integer的hashCode方法返回的就是Integer的值,所以它们的hashCode相同,但是i1和i2指向的是不同对象,所以最后输出的结果是:
i1 hashCode is 2 i2 hashCode is 2 i1, i2 have same hashCode i1, i2 are different
在自定义新的类时,经常会覆写equals方法,但是这里要注意,java有个规定:如果你覆写了某个类的equals方法,那么你一定也要覆写它的hashCode()方法。覆写的原则就是保证如果两个引用调用equals方法时如果返回true,则这两个引用的hashCode()的返回值也需要是一样的。
为什么要这样做,我们可以看一下下面的这种情况:
1 public class test { 2 public static void main(String[] args) { 3 People p1 = new People("zzq", 24); 4 Map<People, Integer> map = new HashMap<People, Integer>(); 5 map.put(p1, 1); 6 System.out.println(map.get(new People("zzq", 24))); 7 } 8 } 9 10 class People { 11 String name; 12 int age; 13 14 public People(String name, int age) { 15 this.name = name; 16 this.age = age; 17 } 18 19 public String getName() { 20 return name; 21 } 22 public void setName(String name) { 23 this.name = name; 24 } 25 public int getAge() { 26 return age; 27 } 28 public void setAge(int age) { 29 this.age = age; 30 } 31 }
这个类中定义了一个其他的类,People。并且新建了一个HashMap<People, Integer>。并且将一个新建的people对象加入其中,然后我们想按照那个People对象的值新建另一个同样值的对象new People("zzq", 24),并且通过新建的对象找到它的值value。但是结果返回为 null 。我们希望它能够返回1。
为什么这里不可以?主要就是因为没有实现hashCode()方法,此时这个类的hashCode方法仍旧是返回内存地址,所以二者的hashCode不同。
此时我们可以看下 get() 方法
1 public V get(Object key) { 2 if (key == null) 3 return getForNullKey(); 4 int hash = hash(key.hashCode()); 5 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 6 e != null; 7 e = e.next) { 8 Object k; 9 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) 10 return e.value; 11 } 12 return null; 13 }
其中首先就是第3行,首先计算出你要找的元素的关键字,以及这个关键字的hashCode,由于此时hashCode是内存地址,所以一开始存入的p1,和后来在get函数输入参数列表中新建的new People()对象,是具有不同的hashCode的。自然得出的hash值也是不一致的,所以一定找不到存放p1的那个哈希表项。所以一定会返回null。
如果我覆写一下hashCode()方法和equals()方法:
1 @Override 2 public int hashCode() { 3 return name.hashCode()+this.age; 4 } 5 6 @Override 7 public boolean equals(Object obj) { 8 return this.name.equals(((People)obj).name) && this.age == ((People)obj).age; 9 }
此时程序就会返回1了。