本人用的jdk版本为1.8。hashCode是Object类中的一个本地方法,这意味着所有的对象继承了它的hashCode方法。废话不多说,先看Object类中对hashCode方法的介绍:
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
开头就写明了该方法的存在的一个重要原因:为了支持哈希表的一些有益特性。同时提出了hashCode()的三点约定:
- 多次执行一个对象的hashCode(),返回的结果都是一样的。
- 若有两个对象obj1和obj2,且obj1.equals(obj2)为true,那么调用这两个对象的hashCode()返回的结果肯定一样。(自己上机测试发现手动改写类的equals方法令两个对象equals为true,但hashCode方法结果不一样,可能手动更改不会影响hashCode方法的返回值)
- 若有两个对象obj1和obj2,且obj1.equals(obj2)为false,则调用这两个对象的hashCode()返回的结果不一定不同,但程序员要知道“equals为false产生不同的hashCode值”能提升hash表的性能。
事实也正是如此,hashCode方法一个最重要的作用就是对所有基于散列的集合的支持。之所以这么说,是因为在类似HashMap这类集合中,put和get等操作对数据的存取都是基于key的hashCode,多说无益,来看HashMap中put和get方法的源代码。想进一步了解HashMap的话可以看:Hashmap底层实现 jdk1.8。
put方法
下面是put方法的部分源代码和hash方法。可以清楚的看到,在put(key,val)时,首先是根据(n-1)&hash(key)来判断要插入的散列表的位置,其实就是靠hash(key)来决定。而hash方法的返回值是由key.hashCode()来决定的,所以key.hashCode()决定了val要插入的位置。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在源代码中我们进一步看到,在决定key、val要插入的位置后(hash表中存的是Node,而Node中包括key和val,不仅仅只有val),如果插入的位置上已经存在元素,那么就要判断要插入的元素和已存在的元素的key是否相同,因为HashMap不允许key重复,如果key重复则要覆盖val。那怎么判断呢?就是根据key的hashCode方法和equals方法来判断的。obj1==obj2表示他们是同一个对象,所以肯定有obj1.equals(obj2)返回true,可以参考:==和equals的区别。
// 这里的hash是hash(key)的返回值
/*通过(n - 1) & hash计算出要插入数据的key在table(散列表)中的散列位置,并让P
指向table中该位置上的原Node对象。如果P为null,就把数据插入到该位置上*/
Node<K,V> p;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
/*根据key的hash值和equals()判断p与插入数据的key是否相同*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //最后的代码会让插入数据覆盖p
else {...}
}
// hashtable 中元素的类型
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
......
}
所以可想而知,如果你修改了一个对象(比如User)的equals()和hashCode()后,那么下面的代码会往hash表中插入一条数据,因为user1和user2的hashCode值一样,就插入到同样的位置,又user1.equals(user2),所以判定重复,直接替换,而不是新插入一条记录。
public class Test5 {
public static void main(String[] args) {
Map<Object,String> map = new HashMap<>();
User user1 = new User("张三", 1);
User user2 = new User("李四", 1);
System.out.println("equals:" + user1.equals(user2));
System.out.println("hashCode值:" + user1.hashCode() + "——" + user2.hashCode());
map.put(user1,"hello");
map.put(user2,"hello");
System.out.println("map中记录数:" + map.size());
}
static class User{
String name;
int age;
public User(String name ,int age){
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object obj) {
User obj1 = (User) obj;
return age==obj1.getAge();
}
@Override
public int hashCode() {
return 1;
}
}
}
get方法
下面是HashMap的get方法的部分源代码,对于map.get(key),首先也是根据(n-1)&hash(key)来确定再tables上的位置,然后根据key的hashCode方法和equals方法来遍历冲突链表来判断的,这里就不赘述了。显然对于上述测试代码,通过map.get(new User("张三");也是获取不到值的。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//table长度大于0且key的在table中的散列位置上的Node不为空,则执行方法
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//若first的key与参数key相同,返回first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
......
}
return null;
}
如何重写hashCode方法
很多时候我们为了省事可能会这样写:
@Override
public int hashCode(){
return 520;
}
但《Effective Java》中不建议,因为它使得该类的每一个对象都有同样的散列码。根据上面的讲解,HashMap是根据key.hashCode()来决定val最终存放在table中的位置,这样会导致当存入以该对象作为key的键值对时,都会映射到同一个散列桶中,使散列表退化为链表。是的本该限行时间运行的程序变成了以平方级时间在运行。对于规模很大的散列表而言,这关系到散列表能否正常工作。
书中也提到了一种重写hashCode()的策略:
生成一个 int 类型的变量 result,并且初始化一个值,比如17
对类中每一个重要字段,也就是影响对象的值的字段,也就是 equals 方法里有比较的字段,进行以下操作:a. 计算这个字段的值 filedHashValue = filed.hashCode(); b. 执行 result = 31 * result + filedHashValue;
hashCode通用约定
在最上面的hashCode方法的注释中介绍了几个通用约定,我们应该在平时开发中遵守这些约定,如果违反的话可能导致对应类无法结合所有基于散列的集合一起正常运作。
-
在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode 方法都必须始终返回同一个值。
-
如果两个对象根据 equals 方法比较是相等的,那么调用这两个对象中的 hashCode 方法都必须产生同样的整数结果。
-
如果两个对象根据 equals 方法比较是不相等的,那么调用者两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果。但是给不相等的对象产生不同的整数散列值,是有可能提高散列表(hash table)的性能。