JDK源码_浅谈HashMap
2021/5/30 12:20:12
本文主要是介绍JDK源码_浅谈HashMap,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
背景
map是所有编程语言都通用并且常用的数据结构,而HashMap更是java中使用的最广泛的map
写这篇博客的初衷:① 学到优秀设计 ② 用好HashMap ③ 跟面试官/同事吹水
Problem before Read:
- HashMap中有哪些优秀的设计?
- HashMap是如何扩容的?
- HashMap1.7和1.8有什么区别?
- HashMap和ArrayList区别?
场景驱动看源码:
- HashMap创建(构造函数)
- HashMap读写
- HashMap扩容
一、HashMap创建
HashMap提供了4个构造方法
// 1. 设置默认阈值(为啥不像第2个构造方法调用第3个?因为其无输入值,无需要做校验以及调整输入的初始值) public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } // 2. 调用第3个构造方法来做校验并且调整初始值为最小2次幂(方便通过位运算来定位目标元素) public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 3. 校验入参及调整初始值为最小2次幂 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } // 4. 通过传入的map进行初始化 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } // ① 如果当前map还未初始化,则将入参map的大小除以阈值,这样该新创建的map的元素刚好处于阈值的边缘避免空间浪费 // ② 如果当前map已经初始化则判断入参map的大小是否超过当前map的阈值,如超过则先进行扩容 // ③ evict在初次初始化map时为false,否则为true // ④ 循环赋值 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
通过分析可得,前三个构造函数仅设置map大小以及阈值,仅有第四个构造函数会进行初始化
二、HashMap读写
相比写操作,读操作非常简单。因此咱们先看看读操作
读操作
//1. 逻辑比较简单,就是调用getNode方法 public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } // 1. 如果数组目标下标有元素并且第一个元素的key为需要查找的,则直接返回 // 2. 如果发现数组目标下标的元素有后续节点。为树节点则遍历树检索数据;为链表则通过遍历链表来检查即可 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; }
写操作
// 1. 操作也很简单,就是调用putVal public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // 1. 如果table数组为初始化,则此时才进行初始化 // 2. 如果计算出数组目标下标上无元素,则直接将写的内容封装成Node写上去 // 3. 如果目标下标已经有元素。若该元素为TreeNode,则以写树的操作往红黑树追加元素; 是链表则遍历链表,若是链表中不存在该值则再尾节点追加,若是链表中存在该值则根据onlyIfAbsent 来决定是否更新value值 // 4. 在链表中添加完节点后会判断链表的节点是否达到8个,到达则进行树化(先追加元素再进行树化) 树化的前提会判断table数组是否已达到64,否则扩容数组,不树化 // 5. 判断是否到达阈值,若到达也进行table数组扩容 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) 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; 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); if (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 key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap扩容
HashMap的扩容是比较核心的设计,设计得不好很容易影响性能
// 方法相对复杂,一点点来啃 // 1. 如果扩容前table的大小处于合理值,则左移进行双倍扩容 // 2. 循环进行数据迁移 ① 旧table 下标只有一个元素则直接赋值给新table ② 下标为TreeNode节点(红黑树)则进行树迁移 ③ 下标为Node(链表) 则进行链表迁移 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; 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 threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = 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) 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; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.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中有哪些优秀的设计
- 延迟初始化内部数组
有可能创建完Map但不立即使用,如果初始化太早了会造成内存资源浪费 - 将HashMap内部数组的大小控制为2的幂
当数组大小为2的幂时,通过&运算的值与取余的效果是一样的且性能更高
因此数组大小控制为2的幂可以提高HashMap定位元素的速度 - 阈值默认为0.75
阈值0.75是时间和空间的一个折中选择,值小了空间利用率低,值大了查询性能低
由于链表的树化长度为8(因为长度为8之前链表性能优于红黑树),通过配合泊松分布推导出当阈值为0.75时发生树化的概率为千万分之一 - 高性能的哈希算法
通过高低位进行异或运算来保留每个key的独有特征,从而实现哈希算法
二、HashMap是如何扩容的
参考前面
三、HashMap1.7和1.8有什么区别
- 1.8引进红黑树,对性能进行优化
- 1.8往链表插入数据时采用尾插入法代替1.7的头插入法,头插入法在并发场景下会出现环
四、HashMap和ArrayList区别
- HashMap底层采用Entry数组来保存数据
ArrayList底层采用Object数组来保存数据 - HashMap是2倍扩容,ArrayList是1.5倍扩容(相比HashMap,ArrayList扩容成本低,因此可不进行过多的扩容)
- HashMap查询数据时,先对key进行哈希运算然后对table数组取余得到目标下标;而ArrayList是直接通过传入目标下标
这篇关于JDK源码_浅谈HashMap的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-27消息中间件底层原理资料详解
- 2024-11-27RocketMQ底层原理资料详解:新手入门教程
- 2024-11-27MQ底层原理资料详解:新手入门教程
- 2024-11-27MQ项目开发资料入门教程
- 2024-11-27RocketMQ源码资料详解:新手入门教程
- 2024-11-27本地多文件上传简易教程
- 2024-11-26消息中间件源码剖析教程
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器