12、HashMap源码分析

一、HashMap简介

HashMap使我们日常开发使用频率最高的一个容器之一,HashMap最早出现在JDK1.2的版本之中。HashMap在JDK1.8之前数据结构使用的是数组+链表,在JDK1.8及以后的数据结构使用的是数组+链表+红黑树。下面从数据结构、扩容机制、如何解决hash冲突、1.7版本与1.8版本差异、整体的源码剖析介绍HashMap。

1、HashMap继承关系

hashmap继承关系

二、HashMap数据结构

在讨论hashmap数据结构之前,我们先分析下hashmap结构组成部分的数据结构,了解其组成部分数据结构特征我们自然就明白了hashmap为什么这样组合。更多数据结构方面参考我的数据结构系列文章

1、数组

   数组的数据结构拥有内存一段固定连续的内存空间,如果数组按照指定下标的查找时间复杂度O(1), 如果通过给定值查找,需要遍历数组做比较时间复杂度为O(n) , 如果采用其他优化的查询算法,比如二分查找时间复杂度为O(logn)。

2、线性链表

   线性链表与数组不同的是其内存空间是逻辑连续的,这样更好的利用内存空间,但是也增加数据的空间复杂度,如果链表随机新增和删除时间复杂度是O(1),如果链表增删改查指定值需要遍历链表时间复杂度为O(n)。

3、Hash结构

3.1、无hash碰撞  

无hash碰撞hash算法时间复杂度是O(1),这是最理想的情况实际情况不存在这种理想的hash算法,解决hash冲突是任何一个hash结构容器的第一面对的问题。

3.2、有hash碰撞

最坏情况下HashMap的hashcode全部相同,查找需要遍历整个hashmap的元素其时间复杂度为O(n)。

4、红黑树

为什么hashmap链表使用红黑树,而不是二叉树或者跳跃表。这有要从二者数据结构说起,二叉树最坏的情况下偏二叉树的形态就是一条链表其时间复杂度为O(n) ,而跳跃表一般用在查找范围的场景,虽然在算法上跳跃表的时间复杂度为O(logn),但是其空间复杂度更高加载需要更多内存算法的稳定性没有红黑树强。跳跃表在redis上有大量的使用,因为在list场景redis有分页查找范围的需求,这点用红黑树就比较费劲。  红黑树数据结构稳定,增、删、改、查场景数据时间复杂度O(logn),在hash冲突比较多的时候,hash链转化为红黑树可以提高hashmap的检索效率。

JDK1.8 HashMap数据结构
#HashMap数据结构
public class HashMap {
  
  #HashMap桶数组结构
  transient Node<K,V>[] table;


  #HashMap 桶 与 链的Node节点元素结构
  static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
  }


  #HashMap红黑树结构
  static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red; 
  }
}

三、HashMap如何解决Hash冲突

1、开放定址法

开放定址法也称再散列法,就是多次其基本思想是:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础产生另一个哈希地址p2,直到找出一个不冲突的哈希地址pi ,然后将相应元素存入其中。解决了hash冲突,但是无法再次通过hashcode找到目标Key,所以这种方案不适合hashmap。 百度百科解释         

2、再哈希法

通过构造出多个不同的求hash的函数,如果出现hash冲突更换不同的hash函数求新的hash直到不再冲突。 解决了hash冲突,但是无法再次通过hashcode找到目标Key,所以这种方案不适合hashmap。所以这种方案也不适合hashmap。

3、链地址法

链地址法是hashmap选择的方案,其将所有hashcode为N的元素构成一个称为同义词链的链表,并将链表的头指针存在哈希表的第N的单元中,因而查找、插入和删除主要在同义词链中进行。解决了hash冲突也可以通过key的hashcode值再次查找,所以这种方案适合hashmap。

4、建立公共溢出区

建立公共溢出区基本思想是,将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律写入溢出表区域中。hashmap里面存在大量的hashcode冲突,这种处理办法冲突区的查找效率非常低下。

四、HashMap扩容问题

4.1、HashMap容量为什么是2的n次方?

1、hashmap容量初始化过程

#HashMap容量初始化
public HashMap(int initialCapacity, float loadFactor) {
        #容量小于0报错
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        #容量大于1073741824不在执行扩容
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                              loadFactor);
        #实例化扩容因子HashMap默认为0.75
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
}


#HashMap扩容算法根据扩容因子扩容,其扩容为2的n次方
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        // 语意等于 n=n | n >>> 2
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        #容量不大于1073741824者使用扩容的因子
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

2、HashMap扩容为啥是2的N次方
 

2、HashMap扩容因子为什么是0.75?

3、HashCode计算为啥右移动16位?

hashcode值计算过程?

hashcode是HashMap最重要的一个环节之一,要求hashcode获取性能高、均匀、碰撞概率低,下面介绍HashMap是如何获取hashcode的过程。

  • ^ (h >>> 16)是什么,有什么用?

   h >>> 16是用来取出h的高16(>>>是无符号右移) ,然后那原码与新计算的码求异或,这样做的目的是让hash散列效果更好。但是为什么是16而不是别的数字?这是一个数学证明题,需要数学高手来解释,本人数学能力浅薄无法证明。  如下展示:

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


#移位运算与异或运算过程
0000 0100 1011 0011  1101 1111 1110 0001  原码
 
>>> 16 
 
0000 0000 0000 0000  0000 0100 1011 0011  右移16位新码


^ 原码与新码 求异或
0000 0100 1011 0011 1101 1011 0101 0010 相同为0相异为1

hashmap如何添加元素?

  • hashmap定位元素之 i = (n - 1) & hash 

在hashmap元素put代码有一段 i = (n - 1) & hash 它具体有什么用?其实他的作用就是让hash分布更均匀,降低hash碰撞的概率。hashmap实现的每个细节都透露这hash的特征,不得不感叹作者的数学功底和设计的精妙,下面表格展示作者为什么这么设计了。至于hashmap为什么初始化长度为16,而不是8,32,64?可能大部分容量为16可以满足大小要求也不会浪费内存。

十进制(n)二进制(n)二进制(n-1)设计总结
\LARGE 2^1{}101这样设计的好处。n-1的二进制结果永远是 1...11...111...1111...11111 这种结果其结果与hashcode求&得出的值不容易重复,降低hashcode碰撞的概率,这点回答了为什么hashmap容器扩容都是\LARGE 2^n{}的原因。
\LARGE ^{2^{2^{^{}}}}10011
\LARGE ^{2^3{^{}}}1000111
\LARGE ^{2^4{_{}}}100001111
  •     hashmap元素添加代码                  
 #HashMap元素插入逻辑 主要有hashmap的扩容、元素插入链式、元素插入红黑树
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        #tab 数组桶  & p是Node节点 & n是tab桶容量 & i是tab索引脚码 
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            #对tab扩容
            n = (tab = resize()).length;
        #tab index=(n-1)&hash 这句就是hash容量为什么是2的n次方的关键点,如果index找不到元素创 
        #建新的Node节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            #创建新的node节点
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            #新节点key的值与hashcode等于旧节点 节点覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            #如果节点属于红黑树节点
            else if (p instanceof TreeNode)
                #当前节点插入红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                #
                for (int binCount = 0; ; ++binCount) {
                    #如果p.next节点为null
                    if ((e = p.next) == null) {
                        #p.next节点赋值为新node节点
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //把链式数组变成树结构变形因子是8
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                #hashMap未实现,在LinkedHashMap会按照访问效率排序,最少访问的靠前,最新访问的 
                靠后
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        #HashMap未实现,LinkedHashMap中被覆盖的afterNodeInsertion方法,用来回调移除最早放入 
       Map的对象
        afterNodeInsertion(evict);
        return null;
    }
  • hashmap链表与红黑树相互变形?

  • 泊松分布理论

        泊松分布百度百科介绍 数学帝自己研究吧。

  • hashmap为什么链长为8转化为红黑树链长为6转化为链表?

       由红黑树与链表的数据结构可知道。空间复杂度方面:红黑树的空间复杂度大约为链表空间复杂度的2倍,意味这红黑树对内存空间消耗更大。时间复杂度方面:由于链表时间复杂度是红黑树的3倍,当数据量大的时候对检索效率影响更大,这对于追求高效的hashmap来说是难以忍受的。综合原因:当链表长度为6时候,综合时间等待不算太长还可以接受。由于一个链表长度为8的概率不足千万分之一概率是非常低的,所以hashmap选择8为因子变换为红黑树。当红黑树长度为6时转化为链表,是因为在hashmap中链表的概率是最大,变形因子为6可以节省内存空间,还可以防止链表和红黑树之间平凡变形给cpu带来负担。

#hashmap链长分布概率-泊松分布理论
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
#hashmap链长度为6的概率
* 6:    0.00001316
* 7:    0.00000094
#hashmap链长度为8的概率
* 8:    0.00000006
* more: less than 1 in ten million

            

  • hashmap扩容因子为啥是0.75?

  • hashmap内存空间利用率

       hashmap扩容因子是0.75也就意味这hashmap有25%的空间闲置,如果这个因子更小就会带来更大的空间浪费。但是如果扩容因子过大,又会导致hashcode碰撞概率增大。

  • hashmap hashcode碰撞概率    

       hashmap定位元素i=(n - 1) & hash,其结果是n的范围内。如果n范围内的值都大量的使用了,其新值与已有的i碰撞的概率就会增加。       

六、HashMap LRU?

 hashmap是不支持LRU算法的,但是其子类LinkedHashMap是支持LRU的,LRU的算法思想是最近访问的数据,下次访问的概率会更高。每次访问这个节点就把这个节点移动到链表头部,当容器满了会从尾部删除。LinkedHashMap数据结构是集成HashMap的双链表实现。

七、Redis Hash处理方案与HashMap处理方案异同?

  • 相同点:

        数据结构都使用,数组+链表的数据结构。当一个hash槽对应一个链长为1的链表时,Redis与HashMap的性能最好。 且其查询的性能都是O(1).

  • 不同点:

         数据结构不完全相同,hashmap数据结构使用数组+链表+红黑树,redis使用的是数组+链表的形式。redis和hashmap对应数据冲突机制不一样,redis的桶槽/链表长度大于5时redis为了保证性能扩容到dictht[1],hashmap当链表长度为8时转化为红黑树,当链表长度为6时候红黑树转化为链表。     

八、HashMap1.7与1.8版本差异

  1. 在JDK1.8如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)
  2. 发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入
  3. 在JDK1.8中,Entry对象被Node对象替代改个名称。

其他相似文章推荐

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值