Java中HashMap的实现原理及常见问题

HashMap作为Map的一种实现方式,会频繁的出现的我们的代码中,那么你知道HashMap具体的工作原理,以及为什么是这样工作的吗,本篇文章将带你了解HashMap的底层原理。

首先当我们得知道HashMap的基本结构,在JDK1.8之前HashMap的结构仅仅是数组+链表,结构如下图所示。

 横方向上表示的是数组,方便实现快速的定位查询。

竖方向上表示的是链表,方便产生冲突时快速的实现插入或删除。

但这样的结构会有一个缺点,当数组上某一点频繁的产生冲突时,就会形成很长的链表。当我们想要查询时,必须从表头遍历到表尾,这样会极大的消耗时间。因此在JDK1.8以后对HashMap的结构进行了优化,形成了数组+链表+红黑树的结构。

当数组上某处的链表结点数超过8个时就会把自动的把链表结构转化为红黑树结构。红黑树避免了普通AVL树会出现的左、右子子树会特别高的情况,同时也权衡了查询以及插入删除的效率。

了解了HashMap的结构,先来看看HashMap的初始化,当我们new一个HashMap对象时,系统默认会如何构造呢?

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  //数组大小  

static final float DEFAULT_LOAD_FACTOR = 0.75f;    //加载因子

首先,HashMap会默认给定几个参数,其中最重要是这两个,DEFAULT_INITIAL_CAPACITY代表默认的数组大小,1左移4位也就是16,DEFAULT_LOAD_FACTOR表示默认的加载因子,值为0.75。

这里就抛出并解决可以两个问题。

问题一:为什么数组大小设置为16?(为什么数组大小为2的倍数?)

              1、JVM在处理左移操作是可以优化性能。

              2、在内存申请中都是以2的倍数申请,可以减少内存碎片的产生。

              3、可以提高散列度,减少冲突。在后面会讲到,数据存放是通过一个计算公式(n-1)&hash,n代表数组长度,hash表示哈希值,如果n为2的倍数例如16,则它的二进制数为0001 0000,n-1则为0000 1111,与hash值按位与后可以保留后四位,减少了冲突的发生。

问题二:为什么加载因子设置为0.75?

               首先要明白什么是加载因子。加载因子代表这Hash数组的填充程度,当数组中的元素个数超过数组大小乘加载因子时(16*0.75=12),则认为该数组饱和了,需要扩充。也就意味着需要在时间与空间上进行取舍。过大的填充因子会导致冲突加大,链表(或红黑树)的查询时间增加,过小的填充因子减少了冲突但会导致数组空间上的浪费。所以0.75是官方给出的相对折中的一个数值。

接着就是关键部分,初始化完成后,我们想要使用put()函数把<K,V>存储到HashMap中,那么这一过程是如何完成的呢?

首先必须获取每一个Key 的Hash值,其源码如下。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

可以看到Hash值是如何产生的,key继承Object类本身会有一个hash值,先调用本身的hash值再与这个hash值右移16位相异或。那么问题来了,为什么不用key本身的hash值呢?这个问题一会再讨论。

有了新的hash值,就会把hash值,key,value等参数丢掉一个putVal()的函数中。由于源码分析起来太过于枯燥复杂,这里大致将一下具体的流程。首先数组中的元素应该是什么类型的,应该有key和value吧,也应该有hash值,还有作为链表它应该有指向下一个结点的next指针,因此我们先得把这些数据封装在一个对象中,即为Node对象,其结构大致如下:

class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        ……
}

封装完了数据,我们就得把它插入到HashMap中去,如何找到自己应该存放的位置,HashMap中给出了这样的公式(n-1)&hash,通过公式计算出数组下标,如果数组空着的,那正好直接放进去就好了。如果数组被占了且为链表,则找到链尾添加到链尾。如果数组被占了且为红黑树,则调用红黑树的插入方法插入。

在这个过程中还伴随这两个if判断。第一个判断,如果链表太长了(长度超过默认的8)则自动把链表转换为红黑树。第二个判断,如果插入后数组元素太多了(超过16*0.75=12),则自动扩充数组,直接再左移一位(扩大一倍)。

整个流程还是十分清晰的,如果感兴趣,可以带着这个流程去看看源码。

最后,我们回到之前的问题,为什么不用key本身的hash值呢?可以看出hash值对于整个存储过程来说是十分重要的,影响着冲突的发生,空间与时间的利用率等诸多问题。所以新的hash值肯定会带来好处:提高散列度,降低冲突的发生。

举个例子,现在有两个key本身的hash值,old_key_1和old_key_2,和优化后的新的hash值new_key_1和new_key_2。

old_key_1 = 0101 0000 0000 1111      经过hash^(hash>>>16)得到     new_key_1 = 0101 0000 0101 1111

old_key_2 = 1101 0000 0000 1111       经过hash^(hash>>>16)得到     new_key_2 = 1101 0000 1101 1111

我们分别用原hash值和新hash值来通过公式(n-1)&  hash计算一下。

old_key_1 = 0101 0000 0000 1111                                                   new_key_1 = 0101 0000 0101 1111

n-1 =            0000 0000 1111 1111                                                    n-1 =              0000 0000 1111 1111

___________________________                                                    ____________________________

                    0000 0000 0000 1111                                                                        0000 0000 0101 1111

 

 

old_key_2 = 1101 0000 0000 1111                                                   new_key_2 = 1101 0000 1101 1111

n-1 =            0000 0000 1111 1111                                                    n-1 =              0000 0000 1111 1111

___________________________                                                    ____________________________

                    0000 0000 0000 1111                                                                        0000 0000 1101 1111

通过对比可以发现,如果直接使用key的hash值计算得到结果一样,这样就会产生冲突。而通过优化后的hash值通过计算得到两个不同的数组下标,提高来数组的散列度,降低了冲突的可能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值