hashcode的前世今生
hashcode
public int hashCode():hashCode是根类Obeject中的方法。
默认情况下,Object中的hashCode() 返回对象的32位jvm内存地址。
也就是说如果对象不重写该方法,则返回相应对象的32为JVM内存地址。且是int类型的散列码。
对象的散列码是为了更好的支持基于哈希机制的Java集合类,例如 Hashtable, HashMap, HashSet 等。
为什么重写hashCode方法?
一般的地方不需要重写hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会 重写hashCode.
就HashMap来说,好比HashMap就是一个大内存块,里面有很多小内存块,小内存块 里面是一系列的对象,可以利用hashCode来查找小内存块 : hashCode % size(小内存块数量),所以当equal相等时,hashCode必 须相等,而且如果是object对象,必须重载hashCode和equal方法。
为什么equals()相等,hashCode就一定要相等,而hashCode相等,却不要求equals相等?
1、因为是按照hashCode来访问小内存块,所以hashCode必须相等。
2、HashMap获取一个对象是比较key的hashCode相等和equal为true。
之所以hashCode相等,却可以equal不等,就比如ObjectA和ObjectB他们都有属性name,那么hashCode都以name计算,所以hashCode一样,但是两个对象属于不同类型,所以equal为false。
说明:
首先java中set 、HashMap貌似包括List等底层的存储都会把存储区域分成n个部分,而具体存在哪个部分是由hashcode决定的,也就是说查询的时候他会通过hashcode 所有小查询范围,所以如果所有的hashcode都一样,你的hashcode返回了一个常量 ,那么结果就是存储进去以后 都存放在一个区域,查询的时候变成了一个链式查询,完全没有效率。
如果你的hashcode返回的是一个随机数,或者不去重写hashcode,那么即使两个对象是一样的,也会出现问题。
为什么需要hashCode?
1、 通过hashCode可以很快的查到小内存块。
2、 通过hashCode比较比equal方法快,当get时先比较hashCode,如果hashCode不同,直接返回false。
Java HashCode()默认是根据
hash算法,一般是通过将该对象的内部地址转换成一个整数来实现的
所以 : 如果你的hashcode返回的是一个随机数,或者不去重写hashcode,那么即使两个对象是一样的,也会出现问题。
举个例子:
Coder c1 = new Coder("demo", 100);
Coder c2 = new Coder("demo", 100);
假定我们已经重写了Coder的equals()方法而没有重写hashCode()方法:
@Override
public boolean equals(Object other) {
if(other == this)
return true;
if(!(other instanceof Coder))
return false;
Coder o = (Coder)other;
return o.name.equals(name) && o.age == age;
}
然后我们构造一个HashSet,将c1对象放入到set中:
Set<Coder> set = new HashSet<Coder>();
set.add(c1);
最后:
System.out.println(set.contains(c2));
我们期望contains(c2)方法返回true, 但实际上它返回了false
c1和c2的name和age都是相同的,为什么我把c1放到HashSet中后,再调用contains(c2)却返回false呢?
这就是hashCode()在作怪了.因为你没有重写hashCode()方法,所以HashSet在查找c2时,会在不同的bucket中查找.
比如c1放到05这个bucket中了,在查找c2时却在06这个bucket中找,这样当然找不到了.
因此,我们重写hashCode()的目的在于,在A.equals(B)返回true的情况下,A, B 的hashCode()要返回相同的值.
接下来就是重写hashcode的时候会用31这个数 一般写法:
@Override
public int hashCode() {
int result = 17;
result = result * 31 + name.hashCode();
result = result * 31 + age;
return result;
}
我的理解是:首先为了尽量让产生hashcode保持唯一,所以一定使用一个素数来做系数(这里的31),但为什么是31而不是别的素数呢,因为:
31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!
所以从效率上 它是2的5次减1,对计算机来说2的乘除操作只需要做位移操作,例如*32就是左移5位。
也就是说31对计算机的角度来说运算更快、切占内存不多不少,而且形成惯例,虚拟机甚至都专门对他做了优化;
质数(prime number)又称素数,有无限个。
一个大于1的自然数,除了1和它本身外,不能被其他自然数(质数)整除,换句话说就是该数除了1和它本身以外不再有其他的因数;
所以常用31做系数算hashcode
HashMap | HashSet |
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
HashMap使用键(Key)计算Hashcode |
HashSet使用成员对象来计算hashcode值, 对于两个对象来说hashcode可能相同, 所以equals()方法用来判断对象的相等性, 如果两个对象不同的话,那么返回false |
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
HashSet:
HashSet实现了Set接口,它不允许集合中出现重复元素(能存入null)。当我们提到HashSet时,第一件事就是在将对象存储在
HashSet之前,要确保重写hashCode()方法和equals()方法,这样才能比较对象的值是否相等,确保集合中没有
储存相同的对象。如果不重写上述两个方法,那么将使用下面方法默认实现:
public boolean add(Object obj)方法用在Set添加元素时,如果元素值重复时返回 “false”,如果添加成功则返回”true”
HashMap:
HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)。Map接口有两个基本的实现
TreeMap和HashMap。TreeMap保存了对象的排列次序,而HashMap不能。HashMap可以有空的键值对(Key(null)-Value(null))
HashMap是非线程安全的(非Synchronize),要想实现线程安全,那么需要调用collections类的静态方法synchronizeMap()实现。
public Object put(Object Key,Object value)方法用来将元素添加到map中。