重新了解一下HashCode这位老熟人,额,其实本汪虽然经常用,但是本汪和他并不熟

郑重声明:
本汪作为一名资深的哈士奇,每天除了闲逛,拆家,就是啃博客了 作为不是在戏精,就是在戏精的路上的二哈 今天就来啃啃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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖啡汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值