郑重声明:
本汪作为一名资深的哈士奇,每天除了闲逛,拆家,就是啃博客了 作为不是在戏精,就是在戏精的路上的二哈 今天就来啃啃HashCode这块小骨头吧 哈哈,就是这么皮!
1. 什么是HashCode?
1.1 就让作为资深的哈士奇的本汪,来带你来了解一下 Hashcode的简单介绍吧
Hashcode,中文哈希码,也称散列码,是把任意长度的输入(又叫做预映射, pre-image),通过散列算法(可以根据实际应用情况改写算法),变换成固定长度的输出,该输出就是散列值。Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
本汪总结下:额,其实就是 任意长度输入+散列算法 = 哈希值
专业的来说,哈希码算法产生hashcode的方式有三种:
1.根据内存地址的不同,hashcode不同;
2.根据String类包含的字符串内容加以特定算法,使得只要字符串所在的堆空间相同,则返回的哈希码也相同
3.如果有两个一样大小insert对象,返回的hashcode相同。
本汪建议:这个了解下就好,后面还会细说
对于哈希算法,我们改写时考虑的除了减少碰撞(String “Aa”,String"BB",他们的hashcode值都是2112,我们称这种情况为哈希冲突,或哈希碰撞),更多的还有以此算法得出的哈希值为Key,进行键值对存储时的优越性(占用空间要少,取值的效率要高,要符合实际的应用场景,取值的频繁程度,直接影响到优化时算法的重心偏倚,jdkt常用哈希算法的核心“31”也并非是最合适的,有时“15”反而更合适)。
本汪强调:这个可是本汪的心得体会,是有点二的想法哦,可以体会下看看
本汪提醒:下面的可是本汪查阅大量资料的结果,非常值得体味
1.2接下来让本汪来带你们看看 HashCode的实现
我们来查看oracle的解析文档:(https://docs.oracle.com/javase/6/docs/api/java/lang/String.html#hashCode())
public int hashCode() Returns a hash code for this string. The hash
code for a String object is computed as s[0]*31^(n-1) + s[1]*31^(n-2)
- … + s[n-1] using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates
exponentiation. (The hash value of the empty string is zero.)
Overrides: hashCode in class Object Returns: a hash code value for
this object. See Also: Object.equals(java.lang.Object), Hashtable
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
这是通用的标准算法,使用int算术,其中s[i]是字符串中的第i+1个字符元素,n是字符串的长度,以及^表示取幂。对于现代的处理器来说,除法和求余数(模运算)是最慢的动作。
选择值31是因为它是奇数质数。如果是偶数且乘法运算溢出,则信息将丢失,因为乘以2等于移位。使用质数的优势尚不清楚,但这是传统的。31的一个不错的特性是乘法可以用移位和减法来代替,以获得更好的性能:31* i == (i << 5) - i。现代VM自动执行这种优化。——《有效的java》
本汪建议:下面的本汪每次看时都会有新的体会,你呢?
2.HashCode在Java开发中的用处
2.1.hashcode()方法,是他最主要的用武之地
hashcode()定义在JDK的Object.java中,这使得Java内定义的所有Class都有默认的hashcode()函数,同时默认的hashcode是本地方法,使用c语言直接将内存地址转为整数返回。
然而在实际开发中,考虑到我们自己所建类需要满足的某些特性,又必须对hashcode()的内置算法进行改进。
以String对hashcode()的改进为例,String字符串类型,重写了hashcode()方法,jdk6源码如下:
/**
* Returns a hash code for this string. The hash code for a
* <code>String</code> object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using <code>int</code> arithmetic, where <code>s[i]</code> is the
* <i>i</i>th character of the string, <code>n</code> is the length of
* the string, and <code>^</code> indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
以字符串“123”为例:
字符‘1’的ascii码为49
hashCode = (49*31+50)*31+51
或写为:
hashCode = (‘1’*31+‘2’)*31+‘3’
大家仔细看,其实可以发现,在字符串“123”中,越是靠近前边的字符,对hashCode取值的影响程度就越大。这就使得有相同前缀的的字符串都放在了邻近的内存空间,这样做的好处:
1.使得hash数组长度尽可能得小,字符串存储所需的内存空间也就尽可能地减少了。
2.由于字符串都尽可能地扎堆存放,对于取值效率也就尽可能地提升了。
这些好处,在以哈希码(也叫散列码)为Key值,进行键值对存储时,就可以很好地突出出来。
本汪总结下:这个,其实很好理解,就上面的嵌套算法而言,很容易看出字符串里元素是越靠前权重越大,至于好处,就更好理解了
2.2 HashCode在HashMap中的应用
在JDK1.8中,HashMap的内部数据结构为数组+链表/红黑树
数组为存储的主体,链表是为了减小哈希冲突的概率,
本汪说明:HashMap的数据插入原理,在这儿先不探讨,我们主要看看HashMap中的哈希算法,以及jdk1.8比较jdk1.7在此处所做的优化
HashMap中的hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。
static final int hash(Object key){
int h;
return (key == null) ? 0 : (h == key.hashCode())^(h >>> 16);
}
本汪介绍:我们都知道2<<3来得出16是最快的方法,位运算在算法优化中是最为推荐的,这里
(h == key.hashCode())^(h >>> 16)为了加大了哈希码的分散程度,降低哈希冲突,采用了位移和异或来提高算法效率,使得算法在高频操作下,得以尽可能高效,这种加大哈希值分散程度的算法也称为扰动函数。
有实验表明,扰动函数可以减少近10%的碰撞,因而jdk1.7做了四次移位和四次异或,但明显jdk1.8觉得扰动做一次就够了,做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。
下面是1.7的hash代码:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
本汪说明:HashSet的哈希实现直接调用了HashMap,这里不再重复说明,可以简单看下源码
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
2.3 HashCode在HashTable中的应用
HashTable,散列表,又叫哈希表,它是站在快速存取的角度设计的,也是一种典型的“空间换时间”的做法。可以看作一个元素非紧密排列的线性表。
本汪再细说下:它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
“”空间换时间”做法,有一个重要的数据————负载因子,解释下:
比如我们存储80个元素,但我们可能为这80个元素申请了100个元素的空间。80/100=0.8,这个数字称为负载因子。
在HashTable的设计中,为了避免遍历性质的线性搜索,以达到快速存取,我们基于结果尽可能随机平均分布的固定函数
为每个元素安排存储位置
在这里,本汪以HashTable的remove()方法为例
public synchronized V remove(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF)% tab.length;
for (Entry<K,V> e = tab[index], prev = null;
e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
Hashtable同样是通过链表法解决冲突,根据hashcode计算索引时将hashcode值先与上0x7FFFFFFF,这是为了保证hash值始终为正数;
本汪建议:具体可以去https://blog.csdn.net/dingjianmin/article/details/79774192看看HashTable实现原理及源码解析
2.5 HashCode在ThreadLocalMap中的应用
ThreadLocalMapd整体的成员变量和HashMap差不多,主要不同就是扩容阈值是2/3而非0.75,Entry的key是弱引用WeakReference<ThreadLocal<?>>
在查询,插入和删除时,和HashMap的实现有很大不同,hashkey冲突时采用线性地址法而非链地址法,并且在所有可能的地方都做了过期Entry的回收工作,并对因为hash冲突而偏移的Entry进行整理。
//这里分两种情况处理,一种是e不为空且key相等,直接返回结果,
另一种调用getEntryAfterMiss
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
//如果在hash槽位没有找到该节点,说名不存在或者冲突后偏移了,因此需要向后顺序搜索,知道出现空槽位为止(空槽为说明后面不可能再存在了)。同时还会顺便做Entry的清理工作
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
//从hashcode位置开始寻找空位并调用replaceStaleEntry插入,或者遇到相同key则更新并直接return
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
//nextIndex向后唤醒搜索数组
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
//如果key为null,就用新key、value覆盖,同时清理旧数据
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果执行到这里,说明i位置是空的,并且需要直接插入
tab[i] = new Entry(key, value);
int sz = ++size;
//进行清理,如果没有清理出空间,就判断是否需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//由于GC会批量回收无效引用,所以set()的循环中发现一个过期槽位,就意味着这个key前面也可能出现了新的过期槽位,所以向前搜索并记录可能存在的可用槽位。这样可以增加空槽位的利用率,从而避免频繁触发rehash。
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//向后搜索,直到null Entry或者有重复key
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果发现重复key,就和过期槽位做替换,从而维持hashtable的顺序性(每个entry离散列位置尽可能更近)
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e
// 从i位置或者slotToExpunge位置进行批量清理
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(
expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果既没有向前搜索到过期槽位,也没有向后搜索到重复key
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果没有找到重复key,就直接替换过期键
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 如果有其它过期键,就进行批量清理操作
if (slotToExpunge != staleSlot)
cleanSomeSlots(
expungeStaleEntry(slotToExpunge), len);
}
//移除操作,计算hashcode,然后从该位置向后遍历直到遇到null槽位,逐个比较key值是否匹配,如果匹配就调用Entry的clear方法,并从散列位置清理旧数据
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
最后推荐一下暴雪的一款碰撞概率极低的Hash算法https://www.cnblogs.com/duzouzhe/archive/2009/10/14/1583359.html
参考文档(顺序无先后之分)
1.百度百科
https://baike.baidu.com/item/%E5%93%88%E5%B8%8C%E7%A0%81/5035512?fromtitle=hashcode&fromid=7482507
2.Oracle开发文档https://docs.oracle.com/javase/6/docs/api/java/lang/String.html#hashCode()
3.花驴
https://blog.csdn.net/qq_36499475/article/details/83895654
4.爷的眼睛雪亮
https://www.cnblogs.com/austinspark-jessylu/p/9549260.html
5.安琪拉的博客https://blog.csdn.net/zhengwangzw/article/details/104889549
6.简书
https://www.jianshu.com/p/b5406082b5ab