这里聊一下HashMap:
HashMap底层数据结构:
HashMap1.7之前数据结构是数组+链表
HashMap1.8之后数据结构加了红黑树(是用来处理hash冲突的)
HashMap1.7之前put的时候使用的是头插法
HashMap1.8之后put的时候使用的是尾插法

HashMap和HashTable的区别:

  • HashMap是线程不安全的;HashTable是线程安全的,内部对方法头添加了synchronized关键字保证同步。
  • 所以HashMap是效率相比HashTable效率高
  • HashMap允许一个null的key存放在0号桶位;而HashTable不允许null的key/value,否则会报错(空指针异常)
  • HashMap默认初始化大小16,扩容为原来的2倍;HashTable默认初始化大小是11,扩容为原来的2n+1。
  • JDK1.8后HashMap数据结构是数组+链表+红黑树;而HashTable还是数组+链表。

什么时候初始化HashMap?
在第一次put操作的时候进行初始化。实际上put方法里面调用putVal方法,在putVal方法里面调用了resize方法,在resize方法里面进行初始化map操作。

初始化数组:默认初始化容量是16。
也可以在定义hashmap的时候有参数,在里面自定义容量;当自定义容量的时候,如果不是2的指数次幂,将会强转至大于自定义的最接近的2的幂值。

获取key所在桶位:不是通过取模数来定位索引的,而是与运算 h & (length - 1).
WHY?因为数据量大的时候,扩容次数增加,这时候位运算效率高于取模。并且因为hashmap的length是2的次方数,所以-1后导致二进制表示低位都是1;(例如16-1:1111)容易计算。

为什么初始容量要是2的幂次方?
主要有两个原因:

  1. 方便进行与运算;在put操作要放在一个桶位的时候用的是h&(length-1);2的幂次方-1用二进制表示的话低位就全是1,容易计算。
  2. 扩容的时候方便元素的移动

HashMap安全吗?
1.8之前HashMap不安全,当线程数多的时候进行put操作可能会造成循环问题,造成数据丢失
1.8进行了优化:用HiHead,HiTail,LoHead,LoTail四个指针进行均匀分割
将当前位置的每一个key进行和旧的大小与:只能是0或者旧size,结果是0就将lo指向,否则就指向Hi。然后将低的直接转移至新的,高的转移到低+旧大小,理论是均匀的。
但是如果是多线程情况下:有可能两个线程同时调用put方法,将一对key-value要放入散列表中,有可能是放入到一个桶位中,在一个位置放入了两次数据,导致不知道那个线程put成功,造成数据覆盖问题。
要实现安全就要用到ConcurrentHashMap:参考我的另外一篇文章ConcurrentHashMap详解

JDK1.7的HashMap死循环问题?
主要原因是JDK1.7的HashMap使用的是头插法。当一个线程扩容完进行数据迁移的时候,另外一个线程进入,完成了扩容和迁移;然后第一个线程进行数据迁移的时候数据是逆序的了(头插法的坏处,迁移一次会造成一个桶位的数据逆序);然而第一个线程取的时候是顺序取,而实际位置是逆序,等循环一遍就会造成死循环问题。

HashMap的put操作
先要进行定位:要找到放在hashmap中的那个桶位。这块主要是给hash(key)后再进行扰动函数处理后得到位置:让key的hash值的高16位也参与运算,使得散列性更好。
put的时候有下面四种情况:

  • 经过hash和扰动后找到对应位置上是空的,那就直接将节点放在位置上即可。
  • 经过hash和扰动后找到对应位置的key和要插入的相同,就要进行replace。
  • 经过hash和扰动后找到对应位置已经链化,但是还不是红黑树;这种情况就要遍历整个链表,添加到末尾,然后判断是否满足转红黑树的条件进行树化操作。
  • 经过hash和扰动后找到对应位置已经链化,已经是红黑树了
    就要进行红黑树中的查询要插入的节点位置,在进行旋转,变色。

下面源码来自于jdk1.8的HashMap源码:

//可以看到如果key是null的话,就放在了map中的0位置
// (h = key.hashCode()) ^ (h >>> 16):将原来的hash值和右移16位后,并且和原来hash值进行异或运算static final int hash(Object key) {int h;//是一个扰动函数;使得hash值的高16位也参与到运算,更加散列return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}//进行put操作,实际是调用putVal方法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;//延迟初始化,第一次调用put的时候加载。if ((tab = table) == null || (n = tab.length) == 0)//实际就是用resize方法进行初始化,如果没有就从0扩容到16,如果有就扩容到两倍n = (tab = resize()).length;//第一种情况,对应位置是空,直接添加if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//第二种情况,找到了要插入的key和这里的key相同的,要replaceif (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) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);//第一个条件:8-1=7;实际是当7的时候进行树化(因为count从0开始)if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//找到了相同key的位置if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}//这里是replace操作,将新value放入即可if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;     //记录表结构修改的次数if (++size > threshold)resize();afterNodeInsertion(evict);return null;}//树化方法final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;//第二个条件:如果没有64个就进行resize扩容操作if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();//说明满足两个条件,else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}}

HashMap的get操作
get是从hashmap中找到key经过hash和扰乱后对应的桶位查找对应的value;主要有三种情况:

  1. 直接当前节点就是对应的;直接返回
  2. 当前桶位是个红黑树;按照红黑树的思想进行查询
  3. 当前桶位链化了;就一个一个查询,如果不是就指向next

源码:来自于jdk1.8的HashMap源码

 //get操作public V get(Object key) {Node<K,V> e;//这里hash就是找到对应的桶位,具体hash算法看上面的源码return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//第一种情况,在头结点找到了if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;//第二种情况是红黑树if ((e = first.next) != null) {if (first instanceof TreeNode)return ((TreeNode<K,V>)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;}

HashMap的remove操作
传入key参数,从hashmap中将对应的节点移除;remove方法主要是先找到对应位置,在进行移除操作;主要有三种情况:

  1. hash过后直接头结点就是要删除的;找到直接指向next
  2. hash过后找到对应的桶位是红黑树;按照左小右大的原则查询到位置,在进行删除操作(里面要涉及到变色,旋转操作),在要判断这个红黑树的节点是否小于非树化的阈值(默认是6);如果小于就要进行红黑树到链表的转化。
  3. hash过后桶位链化了;一个一个遍历,找到后指向next

源码:来自于jdk1.8的hashmap源码

 public V remove(Object key) {Node<K,V> e;//调用核心方法return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;}final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;//查询位置。并且节点就是要删除的节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;else if ((e = p.next) != null) {//第二种情况,是树节点,按照红黑树进行删除if (p instanceof TreeNode)node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {//第三种情况,要遍历整个链表,直到找到位置就breakdo {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}//删除节点if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode)((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)tab[index] = node.next;elsep.next = node.next;++modCount;--size;afterNodeRemoval(node);return node;}}return null;}

HashMap的扩容机制(resize)
扩容要遍历每个桶位,在进行操作,主要有三种种情况:

  1. 当前只有一个节点
    计算在新散列表的位置并且放入
  2. 当前已经是红黑树了
  3. 当前所在节点已经链化了
    因为这里链化的所有节点的低几位都是相同的,都是1,然而扩容后翻倍,再次扰乱hash的时候,就要多算上扩容后的一位,这个时候与运算(hash(key) & length-1)就要比之前多看一位,有的是0,有的是1;0的话就还在新散列表对应位置,而1的话就要加上之前的散列表长度就是对应位置。

链表转移到新散列表过程(链表迁移过程):
将第一个是0的指向loHead,遇到其他等于0的就指向loTail,一直指并且loTail后移;将第一个是1的指向hiHead,遇到其他等于0的就指向hiTail,一直指并且hiTail后移;
将loHead到loTail放到新散列表的原来位置上,将hiHead到hiTail放到新散列表原来位置+原来散列表长度位置即可

源码:来自于jdk1.8的HashMap源码

 final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;//旧散列表容量大于0if (oldCap > 0) {//旧散列表容量已经大于最大容量,就直接返回,不进行扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//就散列表容量左移一位(扩大一倍)还小于最大容量并写旧的容量大于默认容量16,就将旧的阈值左移一位(扩大一倍)作为新的阈值else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//旧散列表容量等于0的情况,说明散列表是空的//旧的阈值大于0,在new HashMap的时候传入参数的情况else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//阈值和容量都等于0,就进行赋值操作else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;//初始化操作@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;//进行扩容if (oldTab != null) {//遍历旧的散列表,操作每一个桶位for (int j = 0; j < oldCap; ++j) {Node<K,V> e;//当前桶位里面有数据if ((e = oldTab[j]) != null) {oldTab[j] = null;//只有一个节点if (e.next == null)//将节点放到新散列表的位置 通过hash & (newCap - 1)计算位置newTab[e.hash & (newCap - 1)] = e;//是红黑树else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//是链表else { // preserve order//低位链表:存放在扩容后数组的下标位置,和旧的相同Node<K,V> loHead = null, loTail = null;//高位链表:存放在扩容后数组的下标位置:旧的下标+扩容前数组长度Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;//相与后是0:就是低位链表if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}//高位链表else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

为什么要引入红黑树?
因为就是要解决链化过长的问题。当hash一直冲突的时候,就会造成某个位置链化过长,查询效率过低。引入红黑树,使得get操作效率提升。

HashMap真的是从8开始链表转红黑树吗?
必须满足两个条件:

  1. 某个链表长度大于8;
  2. hashmap元素长度超过64才可以进行树化。

链表转红黑树:当数组长度<64,优先扩容;当>=64,在判断是否有其中的节点链化达到树化阈值(默认是8),实际链表长度是9.当满足两个条件就给对应位置进行树化为红黑树。

这些都是看源码获取的,要是有错误请指教。

红黑树特性

  • 根节点是黑色的
  • 红色节点的子节点都是黑色的
  • 不存在两个红色节点相邻的
  • 从根节点到任意叶子节点都要经过相同个数的黑色节点。

一般插入节点的时候都默认是红色的,因为如果是黑色的话,哪整个红黑树都是黑色节点,也满足条件,哪不就还是普通的二叉树。

红黑树转换
红黑树的转变主要是三种方式:变色,左旋,右旋。
转换步骤如下:

  • 插入节点默认是红色的,按照二叉树左小右大的特性寻找插入位置。
  • 如果父亲和叔叔节点是红色,将父亲和叔叔改为黑色,爷爷改为红色。
  • 如果不满足就要进行旋转(左旋和右旋)。
  • 当前父节点红,叔叔黑,且当前节点是右子树。以父节点进行左旋。
  • 如果在不满足就要进行右旋(及当前父节点红,叔叔黑,且当前节点是左子树的时候):先把父亲从红变黑,爷爷从黑变红,在以爷爷进行右旋。
    红黑树转换是定死的,想不通可以画图记忆一下。

hashmap底层源码详解相关推荐

  1. HashMap 底层源码详解(jdk1.8)

    目录 HashMap概述 Map家族 哈希表 哈希表扩容 构造方法 put()方法(第一次插入) resize()方法 让数组容量为2次幂的原因 get()方法 get()方法实现原理 put()方法 ...

  2. java源码系列:HashMap底层存储原理详解——4、技术本质-原理过程-算法-取模具体解决什么问题

    目录 简介 取模具体解决什么问题? 通过数组特性,推导ascii码计算出来的下标值,创建数组非常占用空间 取模,可保证下标,在HashMap默认创建下标之内 简介 上一篇文章,我们讲到 哈希算法.哈希 ...

  3. Go 语言 bytes.Buffer 源码详解之1

    转载地址:Go 语言 bytes.Buffer 源码详解之1 - lifelmy的博客 前言 前面一篇文章 Go语言 strings.Reader 源码详解,我们对 strings 包中的 Reade ...

  4. Go bufio.Reader 结构+源码详解

    转载地址:Go bufio.Reader 结构+源码详解 I - lifelmy的博客 前言 前面的两篇文章 Go 语言 bytes.Buffer 源码详解之1.Go 语言 bytes.Buffer ...

  5. 封装成jar包_通用源码阅读指导mybatis源码详解:io包

    io包 io包即输入/输出包,负责完成 MyBatis中与输入/输出相关的操作. 说到输入/输出,首先想到的就是对磁盘文件的读写.在 MyBatis的工作中,与磁盘文件的交互主要是对 xml配置文件的 ...

  6. Redis从精通到入门——数据类型Zset实现源码详解

    Redis数据类型之Zset详解 Zset简介 Zset常用操作 应用场景 Zset实现 源码阅读 Zset-ziplist实现 图解Zset-ziplist Zset-字典(dict) + 跳表(z ...

  7. java的数组与Arrays类源码详解

    java的数组与Arrays类源码详解 java.util.Arrays 类是 JDK 提供的一个工具类,用来处理数组的各种方法,而且每个方法基本上都是静态方法,能直接通过类名Arrays调用. 类的 ...

  8. 【 卷积神经网络CNN 数学原理分析与源码详解 深度学习 Pytorch笔记 B站刘二大人(9/10)】

    卷积神经网络CNN 数学原理分析与源码详解 深度学习 Pytorch笔记 B站刘二大人(9/10) 本章主要进行卷积神经网络的相关数学原理和pytorch的对应模块进行推导分析 代码也是通过demo实 ...

  9. 【卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10)】

    卷积神经网络CNN 实战案例 GoogleNet 实现手写数字识别 源码详解 深度学习 Pytorch笔记 B站刘二大人 (9.5/10) 在上一章已经完成了卷积神经网络的结构分析,并通过各个模块理解 ...

最新文章

  1. Python爬虫入门(5):URLError异常处理
  2. nacos 怎么配置 里的配置ip_Nacos-服务注册地址为内网IP的解决办法
  3. excel数据输入模型前的转换
  4. python中简述对象和类的关系_Python学习笔记(七)对象和类
  5. 常用控制台命令大全-Ubuntu篇
  6. 华为三层交换机路由配置案例_{华为HCNP-RS}三层交换机的配置实例
  7. 什么是连续潮流cpf_2019童装秋冬潮流趋势报告:一文读懂童装潮流四大消费趋势...
  8. ASP.NET Core Razor 页面使用教程
  9. 【COCI 2018/2019 Round #2】Kocka
  10. linux恢复硬盘工具,linux硬盘数据恢复工具
  11. Ajax提交与传统表单提交的区别说明
  12. Scratch编程与科学结合-串联与并联
  13. scratch 大家来找茬
  14. html家庭家谱网页,怎样编写自己小家庭的家谱
  15. Qt for Mac苹果开发中,使用Apple Developer文档
  16. 人的一生要疯狂一次,无论是为一个人,一段情,或一个梦想
  17. 每周分享第 31 期
  18. 如何利用网络赚钱之一
  19. 腾讯股票接口怎样新建一个历史类数据的编程?
  20. 内存压力测试软件,TestMem5内存压力测试

热门文章

  1. 八大基本数据类型(primitive type)
  2. ATAC-seq数据分析(一)
  3. python公众号文章爬虫_Python爬虫爬取微信公众号历史文章全部链接
  4. Linux系统管理 4 Shell的基本应用
  5. 【小记】LaTex 语法说明
  6. Python for循环遍历字典(dict)的方法
  7. sql文件导入mysql数据库出错_如何解决navicat导入sql文件出错的问题
  8. eclipse如何汉化
  9. 优秀产品经理的18种能力
  10. ubuntu Linux 终端的一些快捷键