每逢面试都会被问到 HashMap 源码,那么 HashMap 为什么这么受面试官喜欢呢?它到底是好在了哪里?我们在平时工作中,也会经常使用它,今天,给大家带来一篇 HashMap 的源码分析,也是我个人学习的一点总结,希望可以帮到有需要的小伙伴,如果有写的不对的地方,欢迎随时指出,共同讨论。

写在前面的话:

首先 HashMap 在 JDK1.7 和 JDK1.8中的底层结构是不同的,1.7 中采用的数组+链表的形式,到了1.8 中,引入了红黑树,也就是数组+链表 或者 数组+红黑树,那么为什么要引入红黑树呢?这是因为,如果只是单纯的数组+链表,当我们的链表很长很长的时候,去循环遍历查找,性能会下降,为了优化这个地方,在 1.8 中引入了红黑树,当链表的长度达到指定临界值的时候,就会转换为红黑树的形式存储,从而有效的解决了这个慢的问题。下面,让我们来一探究竟,解开 HashMap 的神秘面纱。

一、HashMap的初始化

老规矩,我们还是从它的初始化过程入手。

Map map = new HashMap();

当我们写下这么一行代码的时候它做了什么呢?首先让我们看一下它内部定义的一些成员变量:

//默认的 HashMap 中数组的长度  16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap 中的数组的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的平衡扩容因子,当数组中的元素达到 3/4时进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转红黑树的临界值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表的临界值
static final int UNTREEIFY_THRESHOLD = 6;
//链表转红黑树的数组长度的临界值
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap 中的数组结果
transient Node<K,V>[] table;
//HashMap中的元素个数
transient int size;
//对 HashMap 操作的次数
transient int modCount;
//扩容的临界值
int threshold;
//实际的扩容值
final float loadFactor;

无参构造函数,初始化了来一个空的数组,默认长度是 16,扩容因子是 0.75f:


/*** Constructs an empty <tt>HashMap</tt> with the default initial capacity* (16) and the default load factor (0.75).*/
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

二、核心方法分析【put method】

我们来看一下 put 的时候,它做了什么?这个地方也是精华所在:

public V put(K key, V value) {// 这里调用了 putVal 方法,有一点需要注意,hash(key) 这个方法是来计算 key 值的return putVal(hash(key), key, value, false, true);
}

下面先来看一下 hash(key)这个方法干了啥?

static final int hash(Object key) {int h;// key.hashCode() == 32 位长度的二进制的值,// h >>> 16 右移 16 后取的前面 16 位的值,再与 32 位的 hashcode进行异或操作return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

延伸一个知识点:

        Java 中的异或(^)操作是怎么运算的?

        简单来说,就是相同为 0,不同为 1,是将二进制按位进行比较。

举个栗子:

/**
* 十进制 1 转换为二进制 为 01
* 十进制 2 准换为二进制 为 10
*  1 ^ 2 = 3
*/
public static void main(String[] args) {String i = Integer.toBinaryString(1);String x = Integer.toBinaryString(2);String y = Integer.toBinaryString(1^2);System.out.println(i); // 01System.out.println(x); // 10System.out.println(y); // 11
}

好了,我们回到上面的话题,h >>> 16 它为什么要右移 16 位呢?这个需要在后面的代码【if ((p = tab[i = (n - 1) & hash]) == null)】分析中进行解答,这里先透个剧,其主要目的,就是为了解决散列分布不均匀的问题。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {// 声明了一些变量Node<K,V>[] tab; Node<K,V> p; int n, i;//初始判断, 第一次进入,tab 为 null if ((tab = table) == null || (n = tab.length) == 0)//resize() 初始化一个长度为 16 的数组,具体源码在下面,来分析一下 resize 方法做了什么n = (tab = resize()).length; // n = 16// 这一行代码也就是上面我们所说的,为什么要向右移 16 位了,通过右移解决了散列分布不均匀问题// 确定插入的 key 在数组中的下标 tab[i = (16 - 1) & hash]// 我们来举个栗子,比如插入的 key 的二进制右移 16 位后取到的值为// 101011111010101//            1111 对应十进制 (n-1 = 15)// &运算       0101 对应十进制 5// 取到数组下标为 5 的值,判断是否为 null,第一次进入为 nullif ((p = tab[i = (n - 1) & hash]) == null)// 通过 newNode对象,将tab[5]赋值 tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;// 通过计算,得到下标位置如果有值的话,判断 node 对象中的 hash 值与传入的是否一致// 如果一致,再判断传入的 key 是否一致,如果都一致,说明是同一个 key,直接进行内容替换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) {// 判断是否到了链表的尾部if ((e = p.next) == null) {// 如果到了链表的尾部,将新的节点添加到了链表的尾部p.next = newNode(hash, key, value, null);// 判断是否满足,链表转红黑树的条件 binCount >= 7if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st// 转红黑树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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

第一次resize():

final Node<K,V>[] resize() {// table nullNode<K,V>[] oldTab = table;// olldCap = 0int oldCap = (oldTab == null) ? 0 : oldTab.length;//原来的扩容因子 0 int oldThr = threshold;// 新的容量和新的扩容因子int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {// 第一次进入这里// 新的容量 16newCap = DEFAULT_INITIAL_CAPACITY;// 新的扩容因子 0.75 * 16 = 12newThr = (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);}// 更新了 扩容值为 12 threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})// 创建了一个容量为 16 的 node 数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//此时,更新了 table 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)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;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;
}

链表转红黑树:

final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// tab 为空或者数组的长度小于 64if (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);}
}

动态扩容:

final Node<K,V>[] resize() {// 假设我们的数字中已经存在 11 个元素(默认初始化是 16 个) [1,2,3,4,5,6,7,8,9,10,11,,,,,,]Node<K,V>[] oldTab = table;// 16    int oldCap = (oldTab == null) ? 0 : oldTab.length;//12int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}//新的容量是原来容量的两倍(oldCap << 1 = 32)else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)// 扩容的临界值变为 原来的 2 倍 (24)newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;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"})// 扩容后创建的新数组长度为 32Node<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)//数组中的元素只有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;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;
}

至此,HashMap 的核心方法已经分析完了,最重要的就是数组转链表、链表转红黑树,下面用一张流程图进行总结:

HashMap 里面其他的方法在这里不进行详细讲解,小伙伴们把核心的 put 方法了解之后,再去看对应的方法,就很容易理解了。

这里有一点需要大家知道,就是红黑树的几个特性,也就是链表转红黑树的时候,是如何转的,其实源码中的操作,就是根据红黑树的规则来进行处理的:

1:每个节点要么是黑色,要么是红色。
2:根节点是黑色。
3:每个叶子节点(NIL)是黑色。
4:每个红色结点的两个子结点一定都是黑色。
5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

以上,是我个人对HashMap源码的学习及一点小小的心得,有写的不对的地方,还望小伙伴们不吝指教。

HashMap 源码分析相关推荐

  1. Java类集框架 —— HashMap源码分析

    HashMap是基于Map的键值对映射表,底层是通过数组.链表.红黑树(JDK1.8加入)来实现的. HashMap结构 HashMap中存储元素,是将key和value封装成了一个Node,先以一个 ...

  2. 查询已有链表的hashmap_源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)=(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  3. 源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)+(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  4. HashMap源码分析(转载)

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

  5. Map接口总结与HashMap源码分析

    Map接口 1.Map,用于保存K-V(双列元素) 2.Map中的Key Value可以是任意引用分类型的数据,会封装到HashMap的Node对象中 3.Map的key不允许重复.原因和HashSe ...

  6. 在参考了众多博客之后,我写出了多达三万字的HashMap源码分析,比我本科毕业论文都要精彩

    HashMap源码分析 以下代码都是基于java8的版本 HashMap简介 源码: public class HashMap<K,V> extends AbstractMap<K, ...

  7. hashmap源码分析及常用方法测试_一点课堂(多岸学院)

    HashMap 简介 底层数据结构分析 JDK1.8之前 JDK1.8之后 HashMap源码分析 构造方法 put方法 get方法 resize方法 HashMap常用方法测试 感谢 changfu ...

  8. HashMap 源码分析与常见面试题

    文章目录 HashMap 源码分析 jdk 1.7 内部常量 静态内部类 Holder 类 构造方法 put 过程 put 整体流程图 jdk 1.8 增加的常量 Node 类 Hash 值计算的变化 ...

  9. HashMap源码分析

    文章目录 简介 继承关系 存储结构 源码分析 属性 Node节点 TreeNode HashMap 构造方法 put 添加方法 待更新 简介 在我们使用数据存储的时候都会有数据结构这种东西,但是传统的 ...

  10. JDK7中HashMap源码分析

    文章目录 JDK7中的HashMap 一.JDK7中HashMap源码中重要的参数 二.JDK7中HashMap的构造方法 三.JDK7中创建一个HashMap的步骤 四.JDK7中HashMap的p ...

最新文章

  1. centos7 更新源 安装ifconfig
  2. python同时同步发送多个请求_python如何实现“发送一个请求,等待多个响应”的同步?...
  3. Java中的equals学习小结
  4. java中JVM内存管理(1)
  5. yml eureka defaultzone 只生效第一个_SpringCloud基础教程(三)-Eureka进阶
  6. Spring原理之代理与动态代理模式总结(四)
  7. vi 多窗口同步滚动--适用于人工文件比较
  8. 弗尤博客(十一)之搜索博文
  9. 横幅新年促销海报PSD模板,拯救年底节日忙
  10. 手机linux服务器控制,手机上如何远程控制Linux服务器?
  11. C语言strcmp函数使用及模拟
  12. 谷歌浏览器弹出Chrome版本太旧解决方式
  13. npm方法创建一个vue项目,引入element插件
  14. 笔记1——海康威视摄像头关闭声音
  15. 一个WEB应用的开发流程 供学习用!
  16. iMessage推广(群发)技术实现
  17. 求n以内的最大素数 ← C++
  18. Ubuntu下Android开发——配置环境,刷机,push
  19. 二叉树存储结构及实现
  20. Jmeter学习文档/使用

热门文章

  1. 分析:谁能抗衡巨人和阿里巴巴(转)
  2. github 中使用 ssh
  3. 传统媒体怎么做好微信社区?
  4. English Learning - L3 Lesson6 NCE2-Dead Return 译文
  5. 技术政策齐发力,电子签章、电子档案、电子发票即将全面普及
  6. java基础-哈希映射和哈希集
  7. 实战:git中正确删除文件的方法-2021.12.07
  8. sql——创建视图和索引
  9. 如何在Word文档中插入一条分隔线?
  10. 高性能非阻塞Web 服务器Undertow