【HashMap源码】jdk1.7/jdk1.8 HashMap源码解读

jdk1.7/jdk1.8 HashMap源码解读

HashMap实现原理

  • 基于hashing原理,通过put(key,value)get(key)方法存储和获取对象

  • 当我们存储对象的时候,把键值对传给put(key,value)方法时,它调用键对象keyhashCode()方法来计算hashCode,然后找到bucket位置,来存储值对象value。

  • 当获取对象时,也是先计算key的hashCode,找到数组中对应位置的bucket位置,然后通过key的equals()方法找到正确的键值对key-value,然后返回值对象value

  • HashMap里有hash碰撞(冲突),HashMap使用链表来解决碰撞问题,当发生了碰撞,对象将会存储在链表的下一个节点中

    HashMap的每个链表中存储的是key-value对象,当两个不同的键对象key的hashCode相同时,那就发生了碰撞(冲突),那它会存储在同一个bucket的位置,bucket的位置存不下这两个点,那么就通过链表来存储。(在jdk8里,链表长度大于8的话就会变成红黑树)

    取数据可通过键对象key的equals()方法用来找到正确的键值对key-value。

HashMap底层数据结构
  • JDK8以前的HashMap的底层数据结构是: 数组+链表,它之所以有相当快的查询速度,主要是因为它是通过Key计算hashCode来决定一维数组中存储的位置,而增删速度靠的是链表保证。

jdk1.7 hashmap

简单来说,HashMap由数组+链表组成的Entry数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好

  • JDK8中HashMap的底层数据结构:用**数组+链表+红黑树的结构来优化,链表长度大于8同时满足HashMap中元素个数大于64则 变红黑树**,长度小于6变回链表

    jdk1.8 hashmap

什么是Hash表?
  • 散列表(Hash Table,也叫哈希表)通过**关键码值(Key Value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度,这个映射函数叫做散列函数**,存放记录的数组叫做散列表
  • hash表里可以存储元素的位置叫做bucket(桶)。
什么是哈希冲突,如何解决?
  • 不同的key值产生了相同的Hash地址,H(key1) == H(key2)
  • 解决方案:
    • 开放地址法:不断地探测序列,查找一个空的单元进行插入。有线性探测、再平方、伪随机
    • 链地址法:对于相同的hashCode值,使用链表进行连接。使用数组来存储每一个链表。HashMap中使用的方案。
    • 公共溢出区法:建立一个特殊的存储空间,专门存储这些冲突的数据,适用数据冲突比较少的情况。
    • 再散列法:准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数 以此类推。

HashMap源码读过吗?

核心成员变量有哪些知道吗?
JDK7中HashMap源码之核心变量
  1. Entry[] table 这个Entry类型的数组存储了HashMap的真正数据。
  2. size大小 代表HashMap内存储了多少个键值对。
  3. capacity容量 实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。
  4. threshold阈值loadFactor装载因子 threshold是通过capacity*loadFactor得到的(threshold = capacity * loadFactor)。当size超过threshold时(刚好相等时不会扩容),HashMap扩容会再次计算每个元素的哈希位置。
  5. entrySet、KeySet和values这三个都是一种视图,真正的数据来自table
JDK8中HashMap源码之核心变量
  1. Node[] table 这个Node类型的数组存储了HashMap的真正数据 static class Node<K,V> implements Map.Entry<K,V>
  2. size大小 代表HashMap内存储了多少个键值对。
  3. capacity容量 实际上HashMap没有一个成员叫capacity,它是作为table这个数组的大小而隐式存在的。
  4. threshold阈值loadFactor装载因子 threshold是通过capacity*loadFactor得到的(threshold = capacity * loadFactor)。当size超过threshold时(刚好相等时不会扩容),HashMap会扩容,哈希计算比之前有改进。
  5. entrySet、KeySet和values这三个都是一种视图,真正的数据来自table
  6. TREEIFY_THRESHOLD 树化阈值 转换成红黑树的临界值,默认8
  7. UNTREEIFY_THRESHOLD 反树化阈值 红黑树转换成链表的临界值,默认6
  8. MIN_TREEIF_CAPACITY 链表被树化的最小数组容量 最小树形化阈值 默认64
为什么到8转为红黑树 到6转为链表?

为什么链表长度为8转为红黑树

  • TreeNode(红黑树中)占用空间是普通Node(链表中)的两倍,为了时间和空间的权衡。
  • 节点的分布频率遵循泊松分布,链表长度达到8个元素的概率为0.00000006

为什么链表长度为6,红黑树就转为链表呢?

  • 若是7,则当极端情况下(频繁插入和删除的都是同一个哈希桶)对一个链表长度为8的哈希桶进行频繁的删除和插入,同样也会导致频繁的树化<=>非树化
插入和获取数据的过程清楚吗?
  • JDK1.7中HashMap插入的过程:— put操作

    在这里插入图片描述

        public V put(K key, V value) {
    	    // 步骤1
            if (table == EMPTY_TABLE) {	//判断如果表空进行初始化
                inflateTable(threshold);
            }
            // 步骤2
            if (key == null)		//判断是否键为空
                return putForNullKey(value);	//遍历table[0]是否存在 e.key == null
            // 步骤3
            int hash = hash(key);	//计算hash值
            int i = indexFor(hash, table.length);	//计算bucket的位置
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {	//遍历链表
                Object k;
                // hash相同 equals比较是真 则替换 返回旧值
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
            modCount++;
            addEntry(hash, key, value, i);	//添加数据 这里包含hash冲突的处理。
            return null;
        }
    
  • jdk7 中的hash函数处理

    final int hash(Object k) {
    /** 
    获取哈希干扰因子,该因子会跟根据HashMap的容量进行变更
    变更情况根据上一步的“final boolean initHashSeedAsNeeded(int capacity)
    ”方法动态变更
    **/ 
        int h = hashSeed;
    //如果为干扰因子不为0,且传入的key类型为String,则使用特定的算法(sun.misc.Hashing.stringHash32((String) k))对该key进行hash计算。并返回
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
    //如果哈希干扰因子为0 或者 k的类型不为String则使用异或操作变更key的hashcode
        h ^= k.hashCode();
    
    //为了减少Hash冲突出现次数进行必要的位干扰,默认负载因子是8.
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    扰动实现复杂,一共九次扰动: 5次异或 加上4次位运算

    void addEntry(int hash, K key, V value, int bucketIndex) {
          if ((size >= threshold) && (null != table[bucketIndex])) {
              //当size大小大于或等于阈值的时候,将会进行扩容操作,将数据元素重新计算位置后放入newTable中,Hash位置会重新计算
              resize(2 * table.length);	//2倍扩容
              hash = (null != key) ? hash(key) : 0;
              bucketIndex = indexFor(hash, table.length);
          }
        
          createEntry(hash, key, value, bucketIndex);	//创建Entry的时候会有hash冲突的代码
    }
    

    不小于threshold阈值直接2倍扩容,Hash位置会重新计算。

    void createEntry(int hash, K key, V value, int bucketIndex) {
          Entry<K,V> e = table[bucketIndex];
        //根据头节点构建----头插法的实现
          table[bucketIndex] = new Entry<>(hash, key, value, e);
          size++;
    }
    
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    

    哈希冲突时链表插入数据使用头插(JDK1.7)

  • JDK7中获取数据的过程get操作

    在这里插入图片描述

     //获取key值为key的元素值  
        public V get(Object key) {  
            if (key == null)//如果Key值为空,则获取对应的值,这里也可以看到,HashMap允许null的key,其内部针对null的key有特殊的逻辑  
                return getForNullKey();  
            Entry<K,V> entry = getEntry(key);//获取实体  
    
            return null == entry ? null : entry.getValue();//判断是否为空,不为空,则获取对应的值  
        }  
    
        //获取key为null的实体  
        private V getForNullKey() {  
            if (size == 0) {//如果元素个数为0,则直接返回null  
                return null;  
            }  
            //key为null的元素存储在table的第0个位置  
            for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
                if (e.key == null)//判断是否为null  
                    return e.value;//返回其值  
            }  
            return null;  
        }  
    

    get方法通过key值返回对应value,如果key为null,直接去table[0]处检索。

    //获取键值为key的元素  
        final Entry<K,V> getEntry(Object key) {  
            if (size == 0) {//元素个数为0  
                return null;//直接返回null  
            }  
    
            int hash = (key == null) ? 0 : hash(key);//获取key的Hash值  
            for (Entry<K,V> e = table[indexFor(hash, table.length)];//根据key和表的长度,定位到Hash桶  
                 e != null;  
                 e = e.next) {//进行遍历  
                Object k;  
                if (e.hash == hash &&  
                    ((k = e.key) == key || (key != null && key.equals(k))))//判断Hash值和对应的key,合适则返回值  
                    return e;  
            }  
            return null;  
        }  
    
  • JDK8中HashMap源码插入的过程:

    在这里插入图片描述

    1. 首先判断table成员是否初始化,如果没有,则调用resize扩容
    2. 通过传入键值对的key的hashCode和容量,马上得到了该映射所在的table数组下标。并通过数组的取下标操作,得到该哈希桶的头节点。
    3. 如果没有发生哈希碰撞(头节点为null),那么直接执行新增操作。
    4. 如果发生了哈希碰撞(头节点不为null),那么分为两种情况:
      1. 如果与桶内某个元素==返回True,或者equals判断相同,执行替换操作(图中的直接覆盖value)
      2. 如果与桶内所有元素判断都不相等,执行新增操作,可能是链表也可能是红黑树的插入。
    5. 链表新增操作后,会有两个判断:
      1. 如果哈希桶是单链表结构,且桶内节点数量超过了TREEIFY_THRESHOLD(8),且size大于等于了MIN_TREEIFY_CAPACITY(64),那么将该哈希桶转换成红黑树结构。(图中链表长度是否大于8)
      2. 链表长度不大于8时,如果新增后size大于了threshold,那么调用resize扩容。
    static final int hash(Object key) { // 计算key的hash值
        int h;
        // 1.先拿到key的hashCode值; 2.将hashCode的高16位参与运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//进行异或
    }
    

    将key的hashCode与该hashCode的无符号右移16位,异或起来得到的。

    扰动实现简单 一共2次扰动 1次是异或 加上1次位运算

  • jdk8中hashmap put方法源码:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
     
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则直接在该索引位置新增一个节点即可
        if ((p = tab[i = (n - 1) & hash]) == null)	//头节点是否为空
            tab[i] = newNode(hash, key, value, null);
        else {
            // table表该索引位置不为空,则进行查找
            Node<K,V> e; K k;
            // 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点即为要查找的目标节点,将p节点赋值给e节点
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法查找目标节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行查找,使用binCount统计链表的节点数
                for (int binCount = 0; ; ++binCount) {
                    // 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
                    if ((e = p.next) == null) {		//一直遍历到链表的尾部,因为链表的尾部是null
                        p.next = newNode(hash, key, value, null);
                        // 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法将链表节点转为红黑树节点,
                        // 减一是因为循环是从p节点的下一个节点开始的
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 8.如果e节点存在hash值和key值都与传入的相同,则e节点即为目标节点,跳出循环
                    if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;  // 将p指向下一个节点
                }
            }
            // 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖该节点的value,并返回oldValue
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); // 用于LinkedHashMap
                return oldValue;
            }
        }
        ++modCount;
        // 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);  // 用于LinkedHashMap
        return null;
    }
    
  • JDK8 hashmap中的get方法

    1. 调用key的hashCode方法,根据返回值定位到map里数组对应的下标
    2. 判断这个数组下标对应的头节点是不是为null,如果是,返回null
    3. 如果头节点不是null,判断这个引用对应对象的key值的equals方法,跟查询的key值进行对比,判断是否为true,如果为true则返回这个对象的value值,否则继续遍历下一个节点。
    4. 如果遍历完map中所有的节点都无法满足上面的判断 则返回null
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;	//计算hash值
    }
     
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 1.对table进行校验:table不为空 && table长度大于0 && 
        // table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {//判断头节点是否为null
            // 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
            if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))// always check first node
                return first;
            // 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    // 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    // 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
                    if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        // 6.找不到符合的返回空
        return null;
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值