硬核HashMap源码分析,HashMap文章中的圣经
一 前言
本篇是继硬核ArrayList源码分析,答应我每天看一遍好么之后进行的第二篇源码分析;学完本篇你将对hashMap的结构和方法有个全面的了解;面试自己有多强,超人都不知道;比如HashMap的扩容机制,最大容量是多少,HashMap链表是如何转到红黑树,HashMap为什么线程不安全,HashMap的key,value是否能null值等等;
知识追寻者(Inheriting the spirit of open source, Spreading technology knowledge;)
二 源码分析
2.1 官方说明实践
- 官方说**HashMap实现了Map接口,拥有所有的Map操作;并且允许key,value都为null;**实践一下是不是这样子;
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>();map.put(null,null);System.out.println(map);//{null=null}}
果然如此没有报错;再加一个null的key和value看看;不贴代码啊了,最终输出还是一个{null=null}
;说明HashMap的key是唯一的;?怎么保证key唯一后面继续分析;
- 继续看官方说明,HashMap与HashTable不同之处是,HashMap不同步,HashTable不允许key,value为null;实践一下
public static void main(String[] args) {Hashtable hashtable = new Hashtable();hashtable.put(null,null);}
控制台红了啊!!!输出内容如下,震惊,HashTable的key,value真的不能为null;
{null=null}Exception in thread "main"
java.lang.NullPointerExceptionat java.util.Hashtable.put(Hashtable.java:459)at test.HashMapTest.main(HashMapTest.java:17)Process finished with exit code 1
点击源码看看,HashTable的put方法特大号的Make开头确保key,value不能为null,synchronized修饰;人生值得怀疑一下,因为知识追寻者以前都没用过Hashtable耍代码;
public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;
- 继续看官方说明,HashMap无法保证Map中内容的顺序;再次动手实践一下,如果如此,到底啥原有让map顺序不一致呢?后续分析
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>();map.put("清晨","娃哭了");map.put("中午","娃饿了");map.put("晚上","娃不想睡觉了");System.out.println(map);//{中午=娃饿了, 晚上=娃不想睡觉了, 清晨=娃哭了}}
后续的官方说明就不是单单能几句话可以描述的懂了,开胃菜完毕,还是看看具体的源码吧,复习的时候记得看下小技巧Tip;
Tip:
HashMap允许key和value都为null;key具有唯一性;
HashMap与HashTable不同之处是,HashMap不同步,HashTable不允许key,value为null;
HashMap无法保证Map中内容的顺序
2.2 空参构造方法分析
测试代码
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>();}
构造方法源码如下,发行HashMap 的加载因子loadFactor
的值是默认值0.75;
public HashMap() {//默认因子DEFAULT_LOAD_FACTOR=0.75;初始化容量为16this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}
走了一遍父类如下
HashMap --> AbstractMap --> Object
看下HashMap的父类图谱,结构倒是很简单对吧;
看到这边,知识追寻者有2个疑问
问题一 : 负载因子0.75是什么?
问题二:初始化容量16在哪里?
先回答一下初始化容量16问题,知识追寻者找到成员变量,默认初始化容量字段值为16;而且默认初始化值必须是2的倍数;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16默认的初始化容量
回答负载因子0.75是指HashMap的一个扩容机制;比如 扩容因子是0.7,假设容量为10;当插入的数据达到第10*0.7 = 7 时就会发生扩容;那么知识追寻者再计算一下HashMap的扩容算法,0.75 * 16 = 12;那么也就是插入12 个数之后HashMap会发生扩容;
Tip: HashMap默认因子0.75;初始化容量16;如果容量是12,插入12个数之后再插入数据会发生扩容;
2.3 HashMap实现原理分析
官方源码的说明如下:HashMap的实现是由桶和哈希表实现,当桶的容量过于庞大时会转为树节点(与TreeMap类似,由于TreeMap底层是红黑树实现,故说桶转为红黑树也是对的);
抛出一个问题什么是桶?
回答桶之前我们先了解一下什么是哈希表(HashTable)
哈希表本质是一个链表数组;经过Hash算法计算后得出的int值找到数组上对应的位置;丢一张图给读者们看看;
知识追寻者真是为读者操碎了心,再丢一个计算hash值的方法给你们;记得温习一下java基础String类的hashCode() 方法;若2个对象相同,hash值相同;若2个字符串内容相同(equals方法),hash值相同;
public static void main(String[] args) {int hash_morning = Objects.hashCode("清晨");int hash_night = Objects.hashCode("晚上");System.out.println("清晨的hash值:"+hash_morning);//清晨的hash值:899331System.out.println("晚上的hash值:"+hash_night);//晚上的hash值:832240}
继续说我们的桶,一个桶就是一个链表中的每个节点,你也可以理解为一个下图中除了tab上的的entry ;看图
当桶的容量达到一定数量后就会转为红黑树,看图
看到这边的读者肯定收获良多了,这还是开胃菜而已,进入我们今天的正题,源码分析篇;
2.4 put方法源码分析
测试代码
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>();map.put("清晨","娃哭了");System.out.println(map);}
首先进入的是put方法里面包含了putVal方法
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);//}
先对key进行了一次hash再作为putVal里面的hash
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
看看putVal方法内容,因为只插入一个数据,索引为空的情况不会走else里面内容,知识追寻者这里省略了;
很清晰的可以看见创建了一个节点(Node<K,V>会在下面给出)放入了tab中,tab也很好理解,就是一个存放Node的列表;其索引 i = (容量 - 1) & hash值
, 本质上就是hash值拿去与的结果,所以是个哈希表;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//evict =trueNode<K,V>[] tab; Node<K,V> p; int n, i;// 声明 树数组, 树节点,n,iif ((tab = table) == null || (n = tab.length) == 0)//table 为空n = (tab = resize()).length; // resize()是 Node<K,V>[] resize() n 就是树的长度 根据初始容量就是 n=16if ((p = tab[i = (n - 1) & hash]) == null) // i = (16-1) & hash ---> 15 & 899342 = 14 ;tab[i] = newNode(hash, key, value, null);// 创建新节点,并插入数据else {.......}++modCount;//集合结构修改次数+1if (++size > threshold)// size=1 ; threshold=12resize();afterNodeInsertion(evict);return null;}
这边做了个图记录一下
Node代码如下
static class Node<K,V> implements Map.Entry<K,V> {final int hash;//哈希值 key value异或的hash值final K key;//键V value;//值Node<K,V> next;//存放下一个节点Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}
...........
知识追寻者又加了一个晚上进去,跑过代码后转为图如下
这么知识追寻者提出个想法,15的二进制是 1111 ,任何一个key ,value 的哈希值进行异或后得到Node的哈希值再与原来的1111相与 也就是取Node哈希值的二进制的后四位为准;tab的容量为16;存进去一个Node后 size 加1,最多也就是只能存储12个值就会发生扩容;这种情况下只要hash值相同就会发生碰撞,然后就会进入else方法,问题产生了,怎么构造2个一样的Node哈希值是个问题; 这边给出了示例如下:
参考链接:https://blog.csdn.net/hl_java/article/details/71511815
public static void main(String[] args) {System.out.println(HashMapTest.hash("Aa"));//2112System.out.println(HashMapTest.hash("BB"));//2112}static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
继续试验,发现 key的hash值相同后,会进入else方法,会在原来 AA 这个Node节点属性next添加一个节点存放Bb;哇塞这不就是实现了一个entry链表了么
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {//evict =trueNode<K,V>[] tab; Node<K,V> p; int n, i;// 声明 树数组, 树节点,n,iif ((tab = table) == null || (n = tab.length) == 0)//table 为空n = (tab = resize()).length; // resize()是 Node<K,V>[] resize() n 就是node的长度 根据初始容量就是 n=16if ((p = tab[i = (n - 1) & hash]) == null) // i = (16-1) & hash ---> 15 & 哈希值 tab[i] = newNode(hash, key, value, null);// 创建新节点,并插入数据else {Node<K,V> e; K k; // 声明 node kif (p.hash == hash && // p这个node 存放 Aa-->a ; hash 2112((k = p.key) == key || (key != null && key.equals(k))))//这边key内容不相同所以没进入,故不是相同节点e = p;else if (p instanceof TreeNode)// 这边 p不是 TreeNode 故没转到红黑树加Nodee = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) { // 默认 桶的数量为0if ((e = p.next) == null) { // 将p这个Node的下一个节点赋值给e为null;链表的开端哟p.next = newNode(hash, key, value, null);// P创建节点存放BB--> b // 其中 TREEIFY_THRESHOLD = 8if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);//树化break;}if (e.hash == hash && //这边还是判定p和 e 是否是同一个Node((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;//key相同会新值会取代旧值afterNodeAccess(e);return oldValue;}}++modCount;//集合结构修改次数+1if (++size > threshold)// size=1 ; threshold=12resize();afterNodeInsertion(evict);return null;}
知识追寻者记录一下,原来key经过hash后如果hash值相同就会在形成一个链表,看代码也知道除了Tab上的第一个Entry,后面每一个Entry的Node都是一个桶;可以看见一个for循环是个无限循环,如果key的hash值相同次数达到8以后就会调用treeifyBin(tab, hash);
知识追寻者又理解了,只要桶的数量达到8后就会将链表节点树化;这就是 数组 + 链表(每个列表的节点就是一个桶) —> 数组 + 红黑树;为什么链表要转为红黑树呢,这就考虑到算法的效率问题,链表时间复杂度o(n),而红黑树的时间复杂度o(logn),树的查找效率更高;
Tip : hashMap 本质是一串哈希表;哈希表首先由数组 + 链表组成;当hash碰撞后会形成链表,每个链表的节点又是一个桶;当桶的数量达到8之后就会进行树化,将链表转为红黑树,此时HashMap结构就是 数组 + 红黑树 ;如果插入相同的key,新的value会取代旧的value;
2.5 扩容源码分析
测试代码
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>(12);}
首先进入HashMap初始化代码
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);// 容量12 ,负载因子0.75}
进入构造器看看;很简单 就是HashMap的最大容量值是2的30次方;其次由于输入的数字是12不是2的次幂会进行扩容,扩容的结果也很简单就是比输出的值大的2的次幂;比12大的2 的次幂就是16;
public HashMap(int initialCapacity, float loadFactor) {//initialCapacity=12,loadFactor=0.75if (initialCapacity < 0)//容量小于0,抛出非法参数异常throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)// MAXIMUM_CAPACITY = 1<<30 也就是 2 的30次方 ;记hashMap最大initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor)) // loadFactor=0.75>0 ,否则抛出非法参数异常throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;//负载因子赋值this.threshold = tableSizeFor(initialCapacity);// 返回一个2次方长度的容量;此时 threshold = 16}
点进入看看tableSizeFor(initialCapacity)是什么;多个无符号右位或操作;其目的就是构造2次幂个1;很简单的计算,读者可以自行计算;
static final int tableSizeFor(int cap) {//cap=12int n = cap - 1;// n =11n |= n >>> 1;// 11无符号右移 1位的结果与11相或; n=15n |= n >>> 2;//n=15n |= n >>> 4;//n=15n |= n >>> 8;//n=15n |= n >>> 16;//n=15return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//15+1=16}
Tip : HashMap 的扩容机制就是比输入容量值大的第一个二次幂
2.6 get源码分析
测试代码
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>(12);map.put("a","b");map.get("a");}
进入get方法
public V get(Object key) {Node<K,V> e;//声明Node 节点ereturn (e = getNode(hash(key), key)) == null ? null : e.value;// 根据key的哈希值获取Node节点返回Value}
进入 getNode(hash(key), key)方法,可以看见进行了优先选取tab上的entry节点;如果不是才会进入链表或者红黑树进行遍历比对hash值获取节点;看过put源码之后get源码都没什么奇点了;
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 声明 节点数组tab , 节点first, e ; n ,kif ((tab = table) != null && (n = tab.length) > 0 &&// tab.length =16 即容量(first = tab[(n - 1) & hash]) != null) { // 与get的计算hash 值一样; hash值= (容量-1)& hash 得到tab索引;然后获取索引值赋值给firstif (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))//直接检查是否是tab上的enrty,是直接就返回Nodereturn 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;}
2.7 hashCode源码分析
测试代码
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>(12);map.put("a","b");map.put("b","c");int hashCode = map.hashCode();System.out.println(hashCode);//4}
进入hashCode,就是遍历每个entrySet的哈希值进行相加作为HashMap的hash值
public int hashCode() {int h = 0;Iterator<Entry<K,V>> i = entrySet().iterator();//迭代器while (i.hasNext())h += i.next().hashCode();//迭代每个entrySet的哈希值相加return h;}
做个验证
public static void main(String[] args) {HashMap<String, Object> map = new HashMap<>(12);map.put("a","b");map.put("b","c");Set<Map.Entry<String, Object>> entries = map.entrySet();entries.stream().forEach(entry->{System.out.println(entry.hashCode());});}
输出内容是1,3;
Tip : HashMap 的hashCode 就是 entry 的hash值之和,其中Node的哈希值由key和value的哈希值之和;
三总结
- HashMap允许key和value都为null;key具有唯一性;
- HashMap与HashTable不同之处是,HashMap不同步,HashTable不允许key,value为null;
- HashMap无法保证Map中内容的顺序
- HashMap默认因子0.75;初始化容量16;初始化容量为12,插入12个数之后再插入数据会发生扩容;
- hashMap 本质是一串哈希表;哈希表首先由数组 + 链表组成;当hash碰撞后会形成链表,每个链表的节点又是一个桶;当桶的数量达到8之后就会进行树化,将链表转为红黑树,此时HashMap结构就是 数组 + 红黑树 ;如果插入相同的key,新的value会取代旧的value;
- HashMap 的扩容机制就是比输入容量值大的第一个二次幂;比如12,扩容后就是16;17扩容后就是32;
- HashMap 的hashCode 就是 entry 的hash值之和,其中Node的哈希值由key和value的哈希值之和;
关注知识追寻者:
硬核HashMap源码分析,HashMap文章中的圣经相关推荐
- Map接口总结与HashMap源码分析
Map接口 1.Map,用于保存K-V(双列元素) 2.Map中的Key Value可以是任意引用分类型的数据,会封装到HashMap的Node对象中 3.Map的key不允许重复.原因和HashSe ...
- Spring IOC 容器源码分析系列文章导读
1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解.在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅 ...
- 查询已有链表的hashmap_源码分析系列1:HashMap源码分析(基于JDK1.8)
1.HashMap的底层实现图示 如上图所示: HashMap底层是由 数组+(链表)=(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...
- 源码分析系列1:HashMap源码分析(基于JDK1.8)
1.HashMap的底层实现图示 如上图所示: HashMap底层是由 数组+(链表)+(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...
- HashMap 源码分析与常见面试题
文章目录 HashMap 源码分析 jdk 1.7 内部常量 静态内部类 Holder 类 构造方法 put 过程 put 整体流程图 jdk 1.8 增加的常量 Node 类 Hash 值计算的变化 ...
- HashMap源码分析
文章目录 简介 继承关系 存储结构 源码分析 属性 Node节点 TreeNode HashMap 构造方法 put 添加方法 待更新 简介 在我们使用数据存储的时候都会有数据结构这种东西,但是传统的 ...
- JDK7中HashMap源码分析
文章目录 JDK7中的HashMap 一.JDK7中HashMap源码中重要的参数 二.JDK7中HashMap的构造方法 三.JDK7中创建一个HashMap的步骤 四.JDK7中HashMap的p ...
- 【Java源码分析】Java8的HashMap源码分析
Java8中的HashMap源码分析 源码分析 HashMap的定义 字段属性 构造函数 hash函数 comparableClassFor,compareComparables函数 tableSiz ...
- Java类集框架 —— HashMap源码分析
HashMap是基于Map的键值对映射表,底层是通过数组.链表.红黑树(JDK1.8加入)来实现的. HashMap结构 HashMap中存储元素,是将key和value封装成了一个Node,先以一个 ...
最新文章
- 安装Windows 64 位 mysql 最新版本解压包中没有data目录和my-default.ini及服务无法启动的快速解决办法...
- 用python画四叶草代码-python turtle工具绘制四叶草的实例分享
- struts2 spring hibernate 原理
- 用Sql添加删除字段,判断字段是否存在的方法
- 使用 Tye 辅助开发 k8s 应用竟如此简单(四)
- 详解Python线程对象daemon属性对线程退出的影响
- 测试基础-04-用例的编写评审
- Oracle循环语句
- python打印9宫格,25宫格等奇数格,且横竖斜相加和相等
- 2017年软考程序员考试填涂答题卡(纸)注意事项
- xcode-instrument
- 清华大学出版社计算机绘谱,清华大学出版社-图书详情-《土木与建筑类CAD技能一级(二维计算机绘图)AutoCAD培训教程》...
- 阎王爷让我给他做个后台管理系统(转)
- 织梦多城市分站站群安装教程【二级域名版本】
- java飞机大战程序图片不显示
- iOS-地图真实坐标表示形式之间转换(double型,int型 互转)
- 细分市场——电视重生 | 《商业价值》杂志
- Oracle数据库wm_concat()函数的使用方法
- 帆软报表动态数据源插件2.0使用教程
- [面经]网易互娱 游戏研发 offer√
热门文章
- 回忆 C/C++ 源码统计分析小工具
- openKylin荣获OSCHINA“2022 年度优秀开源技术团队” 奖项!
- python 爬取下一页_如何使用Beautifulsoup在python中抓取下一页
- 运行lego—loam遇到的问题以及解决方案
- Apache配置域名转发
- Unity|| 如何把生存类游戏设计得更优秀
- 京东技术人才之路:谁会成为京东下一个12年的希望?...
- C语言 - 随机生成数字 和 汉字
- GridView嵌套GridView
- detectron2训练自己的数据集和转coco格式