刷完HashMap源码,我们一起进大厂

打印 上一主题 下一主题

主题 894|帖子 894|积分 2682

不可不知的哈希映射
  1. 引言
  2. hashmap这个东西呢,太老生常谈了
  3. 开发中常用、面试中常问
  4. 总之,很重要。。。。。
  5. 接下来呢
  6. 咱们就一起来看下,里面到底有哪些解不开的东西
复制代码
2.1 HashMap数据结构

目标:
HashMap 概念、数据结构回顾(JDK8和JDK7) & 为什么1.8使用红黑树?
概念:
HashMap 是一个利用散列表(哈希表)原理来存储元素的集合,是根据Key value而直接进行访问的数据结构
在 JDK1.7 中,HashMap 是由 数组+链表构成的。
在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成
回顾: 数组、链表(优势和劣势)

数组:     优势:数组是连续的内存,查询快(o1)                    劣势:插入删除O(N)
链表:     优势:不是连续的内存,随便插入(前、中间、尾部) 插入O(1)            劣势:查询慢O(N)
思考?
  1. 为什么是JDK1.8 是数组+链表+红黑树???
复制代码
HashMap变化历程
1.7的数据结构:链表变长,效率低 了!!!

1.8的数据结构:
数组+链表+红黑树

链表--树(链长度>8、数组长度大于64)
备注:现在重点讲map,不讲树的操作。树在算法课里有详细学习
总结:
JDK1.8使用红黑树,其实就是为了提高查询效率
因为,1.7的时候使用的数组+链表,如果链表太长,查询的时间复杂度直接上升到了O(N)
2.2 HashMap继承体系

目标:梳理map的继承关系
总结
HashMap已经继承了AbstractMap而AbstractMap类实现了Map接口
那为什么HashMap还要在实现Map接口呢?
据 java 集合框架的创始人Josh Bloch描述,这样的写法是一个失误。
在java集合框架中,类似这样的写法很多,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。
显然的,JDK的维护者,后来不认为这个小小的失误值得去修改,所以就这样存在下来


  • Cloneable 空接口,表示可以克隆
  • Serializable 序列化
  • AbstractMap 提供Map实现接口
2.3 HashMap源码深度剖析

1)目标:
通过阅读HashMap(since1.2)源码,我们可以知道以下几个问题在源码是如何解决的
(1)HashMap的底层数据结构是什么?
(2)HashMap中增删改查操作的底部实现原理是什么?
(3)HashMap是如何实现扩容的?
(4)HashMap是如何解决hash冲突的?
(5)HashMap为什么是非线程安全的?
2)测试代码如下
  1. package com.mmap;
  2. import org.openjdk.jol.info.ClassLayout;
  3. import java.util.ArrayList;
  4. import java.util.ConcurrentModificationException;
  5. import java.util.HashMap;
  6. import java.util.List;
  7. import java.util.concurrent.ConcurrentHashMap;
  8. public class MMapTest {
  9.     public static void main(String[] args) {
  10.         HashMap<Integer, String> m = new HashMap<Integer, String>();//尾插
  11.         //断点跟踪put
  12.         m.put(1, "001");
  13.         m.put(1, "002");
  14.         m.put(17, "003");//使用17可hash冲突(存储位置相同)
  15.         System.out.println(ClassLayout.parseInstance(m).toPrintable());
  16.         //断点跟踪get
  17.         System.out.println(m.get(1));//返回002(数组查找)
  18.         System.out.println(m.get(17));//返回003(链表查找)
  19.         //断点跟踪remove
  20.         m.remove(1);//移除
  21.         System.out.println(m);
  22.         m.remove(1, "002");//和上面的remove走的同一个代码
  23.     }
  24. }
复制代码
3)关于hashMap基本结构的验证
先来个小验证,几乎地球人都知道map是 数组 + 链表 结构,那我们先来验证一下

再来看debug结果:

验证了基本结构,那为啥1和17就在一块了?到底谁和谁放在一个链上呢?内部到底怎么运作的?往下看 ↓
2.3.1  成员变量与内部类

目标:先了解一下它的基本结构
回顾:位运算(下面还会频繁用到)
<blockquote>
1 1;        n |= n >>> 2;        n |= n >>> 4;        n |= n >>> 8;        n |= n >>> 16;  //到这一步n已经各个位都是1了。              //范围校验,小于0返回1,大于最大值返回最大值,绝大多数正常情况下,返回n+1,也就是10000……        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;    }                                [/code]调试案例:
  1.                
  2.                 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16,默认数组容量:左位移4位,即16
  3.     static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量:即2的30次幂
  4.     static final float DEFAULT_LOAD_FACTOR = 0.75f;//负载因子:扩容使用,统计学计算出的最合理的
  5.     static final int TREEIFY_THRESHOLD = 8;//链表转红黑树阈值
  6.     static final int UNTREEIFY_THRESHOLD = 6;//当链表的值小<6, 红黑树转链表
  7.                 ……
  8.     transient Node<K,V>[] table;//HashMap中的数组,中间状态数据
  9.     transient Set<Map.Entry<K,V>> entrySet;//用来存放缓存,中间状态数据;
  10.     transient int size;//size为HashMap中K-V的实时数量(重点),注意!不是table的长度!
  11.     transient int modCount;//用来记录HashMap的修改次数,几个集合里都有它
  12.     int threshold;//扩容临界点;(capacity * loadFactor)(重点)
  13.     final float loadFactor;//负载因子  DEFAULT_LOAD_FACTOR = 0.75f赋值
复制代码
4)总结:
map的构造函数没有你想象的那么简单!

  • 无参构造时,容量=16,因子=0.75。第一次插入数据时,才会初始化table、阈值等信息
  • 有参构造时,不会容忍你胡来,会取大于但是最接近你容量的2的指数倍(想一下为什么?提示:和扩容规则有关)
  • 无论哪种构造方式,扩容阈值最终都是 =(容量*因子)
2.3.3 HashMap插入方法

目标:图解+代码+断点分析put源码
1)先了解下流程图
2)关于key做hash值的计算
当我们调用put方法添加元素时,实际是调用了其内部的putVal方法,第一个参数需要对key求hash值
  1. static class Node<K,V> implements Map.Entry<K,V> {//数组和链表上的节点,1.8前叫Entry
  2.         final int hash;//扰动后的hash
  3.         final K key;//map的key
  4.         V value;//map的value
  5.         Node<K,V> next;//下个节点
  6.         Node(int hash, K key, V value, Node<K,V> next) { //构造函数,没啥说的
  7.             this.hash = hash;
  8.             this.key = key;
  9.             this.value = value;
  10.             this.next = next;//链表下一个
  11.         }
  12. }
  13.                
复制代码
小提问:map里所谓的hash是直接用的key的hashCode方法吗?
  1.     public HashMap() {
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR; //  负载因子DEFAULT_LOAD_FACTOR = 0.75f
  3.     }
复制代码
图解:
结论:使用移位和异或做二次扰动,不是直接用的hashCode!
3)核心逻辑
  1.     public HashMap(int initialCapacity) {
  2.         this(initialCapacity, DEFAULT_LOAD_FACTOR);
  3.     }
复制代码
4)重点(寻址计算):
接上文,关于hash值取得后,放入tab的哪个插槽,也就是所谓的寻址我们重点来讲
  1.     public HashMap(int initialCapacity, float loadFactor) {
  2.               //赋值,多了一些边界判断
  3.         if (initialCapacity < 0)
  4.             throw new IllegalArgumentException("Illegal initial capacity: " +
  5.                                                initialCapacity);
  6.         if (initialCapacity > MAXIMUM_CAPACITY)
  7.             initialCapacity = MAXIMUM_CAPACITY;
  8.         if (loadFactor <= 0 || Float.isNaN(loadFactor))
  9.             throw new IllegalArgumentException("Illegal load factor: " +
  10.                                                loadFactor);
  11.         this.loadFactor = loadFactor;
  12.         this.threshold = tableSizeFor(initialCapacity);  // 注意,map里是没有capacity这个类变量的!
  13.     }
复制代码
我们还是以开始的例子,1和17为例,他们的hash计算后正好是1和17本身,我们可以验证一下
  1.                 //capacity函数,初始化了table,就是table的length,否则取的是threshold
  2.                 final int capacity() {
  3.         return (table != null) ? table.length :
  4.             (threshold > 0) ? threshold :
  5.             DEFAULT_INITIAL_CAPACITY;
  6.     }
  7.                 //带参数的初始化,其实threshold调用的是以下函数:
  8.                 //这是什么神操作???
  9.                 //其实是将n转成2进制,右移再和自己取或,相当于把里面所有的0变成了1
  10.                 //最终目的:找到>=n的,1开头后面全是0的数。如果n=111 , 那就是 1000 ; 如果n=100,那就是它自己
  11.                 //而这个数,恰好就是2的指数,为后面的扩容做铺垫
  12.                 static final int tableSizeFor(int cap) {
  13.         int n = cap - 1;
  14.         n |= n >>> 1;
  15.         n |= n >>> 2;
  16.         n |= n >>> 4;
  17.         n |= n >>> 8;
  18.         n |= n >>> 16;  //到这一步n已经各个位都是1了。
  19.               //范围校验,小于0返回1,大于最大值返回最大值,绝大多数正常情况下,返回n+1,也就是10000……
  20.         return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  21.     }
  22.                
  23.                
复制代码
开始位运算
  1. package com.mmap;
  2. public class TableSizeTest {
  3.     public static void main(String[] args) {
  4.         System.out.println(tableSizeFor(9));  //9的二进制更能看出以下变化
  5.     }
  6.     static final int tableSizeFor(int cap) {
  7.         System.out.println(Integer.toBinaryString(cap));  //1001
  8.         int n = cap - 1;
  9.         System.out.println(Integer.toBinaryString(n)); //1000
  10.         n |= n >>> 1;   //无符号右移,前面补0
  11.         System.out.println(Integer.toBinaryString(n)); //右移再或,1100
  12.         n |= n >>> 2;
  13.         System.out.println(Integer.toBinaryString(n)); //再移动2位, 1111
  14.         n |= n >>> 4;
  15.         System.out.println(Integer.toBinaryString(n)); //就这么长,再迁移也是1111
  16.         n |= n >>> 8;
  17.         System.out.println(Integer.toBinaryString(n));
  18.         n |= n >>> 16;
  19.         System.out.println(Integer.toBinaryString(n)); //Integer的最大长度32位,16折半后迁移全覆盖
  20.         System.out.println(Integer.toBinaryString(n+1));
  21.         return  n + 1;  //+1后变为 10000 ,也就是16 , 2的4次方
  22.     }
  23. }
复制代码
思考:为什么不用mod(模运算)进行寻址?mod也能保证不会超出数组边界,岂不是更简单直观?
  1.    public V put(K key, V value) {
  2.         return putVal(hash(key), key, value, false, true);//调用Map的putVal方法
  3.     }
复制代码
跑一下试试?
结论:
一切为了性能
2.3.4 HashMap扩容方法

目标:图解+代码(map扩容与数据迁移)
注意:扩容复杂、绕、难
备注:线程不安全的
图解: 假设我们  new HashMap(8)
迁移前:长度8      扩容临界点6(8*0.75)

迁移过程


核心源码resize方法
  1.     static final int hash(Object key) {
  2.         int h;
  3.               //【知识点】hash扰动
  4.         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  5.     }
复制代码
总结(扩容与迁移):
1、扩容就是将旧表的数据迁移到新表
2、迁移过去的值需要重新计算下标,也就是他的存储位置
3、关于位置可以这样理解:比如旧表的长度8、新表长度16
旧表位置4有6个数据,假如前三个hashCode是一样的,后面的三个hashCode是一样的
迁移的时候;就需要计算这6个值的存储位置
4、如何计算位置?采用低位链表和高位链表;如果位置4下面的数据e.hash & oldCap等于0,
那么它对应的就是低位链表,也就是数据位置不变
5、 e.hash & oldCap不等于0呢?就要重写计算他的位置也就是j + oldCap,(4+8)= 12,就是高位链表位置(新数组12位置)
2.3.5 HashMap获取方法

目标:图解 (这个简单!)
获取流程
get主方法
  1.     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2.                    boolean evict) {//onlyIfAbsent:true不更改现有值;evict:false表示table为创建状态
  3.               //几个临时变量:
  4.               //tab=数组,p=插槽指针,n=tab的长度,i数组下标
  5.         Node<K,V>[] tab; Node<K,V> p; int n, i;
  6.         if ((tab = table) == null || (n = tab.length) == 0)//数组是否null或者==0,第1次put为空
  7.                   //初始化数组(or扩容),所以table是在这里初始化的,不是new的时候!
  8.                   //初始时,n=16
  9.             n = (tab = resize()).length;  // resize,下面单独讲扩容
  10.              
  11.               //【知识点】为何1 与 17 在一个槽上!秘密就藏在寻址这里
  12.         if ((p = tab[i = (n - 1) & hash]) == null)//寻址:(n - 1) & hash(重要!)
  13.             tab[i] = newNode(hash, key, value, null);//当前插槽没有值,空的!将新node直接扔进去
  14.         else {  //有值,说明插槽上发生了碰撞,需要追加成链表了!
  15.                   //还是临时变量
  16.                   //e=是否找到与当前key相同的节点,找到说明是更新,null说明是新key插入
  17.                   //k=临时变量,查找过程中的key在这里暂存用
  18.             Node<K,V> e; K k;  
  19.             if (p.hash == hash &&  //如果正好,插槽第一个节点(p),跟插入的key相同
  20.                 ((k = p.key) == key || (key != null && key.equals(k))))
  21.                 e = p; //就将它赋值给e,注意!这时候还没覆盖上去,只是标记到e,发现了相同key的节点!
  22.             else if (p instanceof TreeNode) //如果不是这个key,但是类型是一个红黑树节点
  23.                       //这说明当前插槽的链很长,已经变成红黑树了,就调putTreeVal,扔到这颗树上去
  24.                       //树的插入,这里不赘述,在数据结构课中有
  25.                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  26.             else {//如果都不是以上情况,那就是链表了
  27.                 for (int binCount = 0; ; ++binCount) {
  28.                     if ((e = p.next) == null) {//顺着链表一直往后跳,直到遍历到尾巴节点
  29.                         p.next = newNode(hash, key, value, null);//然后把key封装成新node追加到尾巴上
  30.                         if (binCount >= TREEIFY_THRESHOLD - 1) //链表长度计数如果>8转红黑树
  31.                             treeifyBin(tab, hash);//转成树
  32.                         break;
  33.                     }
  34.                     if (e.hash == hash &&
  35.                         ((k = e.key) == key || (key != null && key.equals(k))))
  36.                         break;// 如果遍历过程中找到相同key,那就赋给e,break跳出for循环,执行后面的逻辑
  37.                     p = e;
  38.                 }
  39.             }
  40.             if (e != null) { // 如果e非空,说明前面一顿猛如虎的操作后,找到了相同的key
  41.                 V oldValue = e.value;
  42.                 if (!onlyIfAbsent || oldValue == null)
  43.                     e.value = value;// 看看onlyIfAbsent,如果让覆盖那就覆盖,不让那就算了
  44.                 afterNodeAccess(e);
  45.                 return oldValue;// 返回覆盖前的value值,也就是put方法的返回值
  46.             }
  47.         }
  48.         ++modCount;//用来记录HashMap的修改次数
  49.         if (++size > threshold)//key的数量是否大于阈值
  50.             resize();//如果size大于threshold,就需要进行扩容
  51.         afterNodeInsertion(evict);
  52.         return null;
  53.     }
复制代码
getNode方法
  1.     final Node getNode(int hash, Object key) {        Node[] tab; Node first, e; int n; K k;  //一堆临时变量,不管他        if ((tab = table) != null && (n = tab.length) > 0 &&            (first = tab[    public HashMap(int initialCapacity, float loadFactor) {
  2.               //赋值,多了一些边界判断
  3.         if (initialCapacity < 0)
  4.             throw new IllegalArgumentException("Illegal initial capacity: " +
  5.                                                initialCapacity);
  6.         if (initialCapacity > MAXIMUM_CAPACITY)
  7.             initialCapacity = MAXIMUM_CAPACITY;
  8.         if (loadFactor <= 0 || Float.isNaN(loadFactor))
  9.             throw new IllegalArgumentException("Illegal load factor: " +
  10.                                                loadFactor);
  11.         this.loadFactor = loadFactor;
  12.         this.threshold = tableSizeFor(initialCapacity);  // 注意,map里是没有capacity这个类变量的!
  13.     }]) != null) {                  // 如果table对应的索引位置上有值            if (first.hash == hash &&                 ((k = first.key) == key || (key != null && key.equals(k))))                return first;// 看下第一个元素的key是不是要查找的那个,是的话,返回即可            if ((e = first.next) != null) {//如果后面还有数据,那就继续遍历                if (first instanceof TreeNode)                    return ((TreeNode)first).getTreeNode(hash, key);//树查找                do { //链表查找!!!!!                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        return e;                } while ((e = e.next) != null);            }        }        return null;    }
复制代码
总结:
查询思路比较简单,如果是数组直接返回、如果是红黑实例,就去树上去找,最后,去做链表循环查找
2.3.6 HashMap移除方法

目标:图解+断点分析remove源码
移除流程

tips:
两个移除方法,参数上的区别
走的同一个代码
移除方法:一个参数
  1. Integer i = new Integer(1);
  2. Integer j = new Integer(17);
  3. System.out.println(i.hashCode() ^ i.hashCode()>>16);  //1
  4. System.out.println(j.hashCode() ^ j.hashCode()>>16);  //17
复制代码
移除方法:二个参数
  1. 默认n=16,n-1也就是15,二进制是 1111
  2. 那么 15 & 1
  3. 1 1 1 1
  4. 0 0 0 1
  5. 与运算后 = 1
  6. 再来看15 & 17,17是 10001
  7.   1 1 1 1
  8. 1 0 0 0 1
  9. 与运算后 = 1
  10. 所以,1和17肯定会落在table的1号插槽上!两者会成为链表,解释了我们前面的案例
  11. 原理:不管你算出的hash是多少,超出tab长度的高位被抹掉,低位是多少就是你所在的槽的位置,也就是table的下标
复制代码
核心方法removeNode
  1.     /**     * Implements Map.remove and related methods     *     * @param hash 扰动后的hash值     * @param key 要删除的键值对的key,要删除的键值对的value,该值是否作为删除的条件取决于matchValue是否为true     * @param value key对应的值     * @param matchValue 为true,则当key对应的值和equals(value)为true时才删除;否则不关心value的值     * @param movable 删除后是否移动节点,如果为false,则不移动     * @return 返回被删除的节点对象,如果没有删除任何节点则返回null     */    final Node removeNode(int hash, Object key, Object value,                               boolean matchValue, boolean movable) {        Node[] tab; Node p; int n, index;        if ((tab = table) != null && (n = tab.length) > 0 &&            (p = tab[index =     public HashMap(int initialCapacity, float loadFactor) {
  2.               //赋值,多了一些边界判断
  3.         if (initialCapacity < 0)
  4.             throw new IllegalArgumentException("Illegal initial capacity: " +
  5.                                                initialCapacity);
  6.         if (initialCapacity > MAXIMUM_CAPACITY)
  7.             initialCapacity = MAXIMUM_CAPACITY;
  8.         if (loadFactor <= 0 || Float.isNaN(loadFactor))
  9.             throw new IllegalArgumentException("Illegal load factor: " +
  10.                                                loadFactor);
  11.         this.loadFactor = loadFactor;
  12.         this.threshold = tableSizeFor(initialCapacity);  // 注意,map里是没有capacity这个类变量的!
  13.     }]) != null) {  //注意,p是当前插槽上的头节点!                  //第一步:查,和上面的get操作一毛一样            Node node = null, e; K k; V v;            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                node = p;// 如果key相同,说明是头结点,将它赋给node            else if ((e = p.next) != null) {                      //否则,沿着next一直查找                if (p instanceof TreeNode)                    node = ((TreeNode)p).getTreeNode(hash, key);//红黑查找                else {                    do { //链表查找                        if (e.hash == hash &&                            ((k = e.key) == key ||                             (key != null && key.equals(k)))) {                            node = e;  //如果找到,赋值给node,并终止                            break;                        }                        p = e; // 如果没找到,赋值给p,继续下一轮。                    } while ((e = e.next) != null);                    // 最终while结束的时候,p(前置)-> node(要被删)                }            }                            //第二步:删                  //如果node不为空,说明根据key匹配到了要删除的节点            if (node != null && (!matchValue || (v = node.value) == value ||                                 (value != null && value.equals(v)))) {                if (node instanceof TreeNode)                    ((TreeNode)node).removeTreeNode(this, tab, movable);//红黑删除                else if (node == p)// 不是树,那如果 node == p 的意思是该node节点就是首节点                    tab[index] = node.next;// 删掉头节点,第二个节点上位到数组槽上                else  // p是node的前置,说明上面查找的时候走的do while                    p.next = node.next;//如果不是,那就将p的后指针指向node的后指针,干掉node即可                ++modCount;//HashMap的修改次数递增                --size;// HashMap的元素个数递减                afterNodeRemoval(node);                return node;//返回删除后的节点            }        }        return null;//找不到删除的node,返回null    }
复制代码
总结:

  • 移除和查询路线差不多,找到后直接remove
  • 注意他的返回值,是删除的那个节点的值
拓展:
为什么说HashMap是线程不安全的
我们从前面的源码分析也能看出,它的元素增删改的时候,没有任何加锁或者cas操作。
而这里面各种++和--之类的操作,显然多线程下并不安全
那谁安全呢?下节课我们分析
本文由传智教育博学谷 - 狂野架构师教研团队发布
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力
转载请注明出处!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

不到断气不罢休

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表