基于JDK版本:1.8
一、核心概念
数据结构:从jdk1.8之后,HashMap采用数组+链表+红黑树(Red-Black Tree)。因为这个特性,HashMap的查询时间复杂度理论上为O(1),但实际上是:O(1)+O(n)+O(logn),因为链表长度大于(TREEIFY_THRESHOLD = 8 )会自动转成红黑树,所以当数据Key的哈希值冲突时,时间复杂度将会达到O(logn)。
key和value可以为null,但key只能存在一个null,位于组数的0位置:因为hash(key)方法,当key=null ,返回0。存储对象时,存储为key的对象需要重写equals()方法。
线程不安全,多线程高并发情况下,容易形成 循环链表。高并发场景可以使用Collections.synchronizedMap(Map map) 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。
HashMap初始容量为16,超过:扩容阈值(threshold) = 哈希表容量 * 加载因子(load factor) 进行扩容, 每次扩容2的倍数。且保证容量为2^n个数 。
HashMap JDK1.8 中扩容后,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap。在 1.7的时候扩容后,链表的节点顺序会倒置,1.8则不会出现这种情况。
hash方法的区别:HashMap 会对 key 的 hashCode 返回值做进一步扰动函数处理。
1.7 中扰动函数使用了 4次位运算 + 5次异或运算;
1.8 中降低到 1次位运算 + 1次异或运算;;
扰动处理后的 hash 与 哈希表数组length -1 做位与运算得到最终元素储存的哈希桶角标位置。
HashMap数据结构
二、关键参数
//默认初始化容量,保证为2的倍数。
static final int DEFAULT_INITIAL_CAPACITY = 1 <4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 <30;
//默认加载因子:如果因子太小,则会频繁扩容,因子太大,则键冲突多,转成链表或树。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//触发转成树时链表(桶)的临界阈值,大于2,小于8.
static final int TREEIFY_THRESHOLD = 8;
//转成链表时树的个数临界阈值
static final int UNTREEIFY_THRESHOLD = 6;
//转成树时数组的个数,至少为4*TREEIFY_THRESHOLD,才能避免扩容与转换成树冲突。
//在treeifyBin()方法内有判断
static final int MIN_TREEIFY_CAPACITY = 64;
//加载因子,如果未指定使用的是,DEFAULT_LOAD_FACTOR——无参构造方法
final float loadFactor;
//库容阈值 = 容量 * 加载因子
int threshold;
//存储哈希桶的数组,哈希桶中装的是一个单链表或一颗红黑树,长度一定是 2^n
transient Node[] table;//所有键值对的Set集合 区分于 table 可以调用 entrySet()得到该集合transient Set> entrySet;//HashMap中存储的键值对的数量注意这里是键值对的个数而不是数组的长度(table.length)transient int size;////操作数记录 为了多线程操作时 Fast-fail 机制transient int modCount;
三、主要方法
构造方法
主要决定两个参数:loadFactor和threshold。
其中threshold通过方法:tableSizeFor(initialCapacity)得到
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}该方法永远返回一个 与给定cap最近的且是2的倍数 ,比如:cap=11,返回16(2^4)。
hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}原理:把 key 的 hashCode 方法返回值右移16位,即丢弃低16位,高16位全为0 ,然后在于 hashCode 返回值做异或运算,即高 16 位与低 16 位进行异或运算,这么做可以在数组 table 的 length 比较小的时候,也能保证 高低Bit都参与到 hash 的计算 中,同时不会有太大的开销,扰动处理次数也从 4次位运算 + 5次异或运算 降低到 1次位运算 + 1次异或运算。
添加方法 putVal
这个方法属于向HashMap添加元素的基础方法,关联方法:put/putMapEntries/putIfAbsent。
该方法分三个核心点:
- 空,在数组直接添加
- 红黑树:把需要添加的数据转成红黑树。
- 链表:如果链表元素超过:TREEIFY_THRESHOLD(8) 转成树。
- 链表:遍历元素,为空则添加,存在key一致,如果可以覆盖oldValue,则覆盖后返回oldValue。
- 通过index = (n - 1) & hash,获取HashMap数组对应的节点
- 对当前节点判断:
- 如果超过了扩容的阈值,进行扩容resize()。
//hash:key的hash值(使用方法hash(key))
//onlyIfAbsent:如果true,不替换原值,方法putIfAbsent()传true
//evict:如果是false代表HashMap正在初始化
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {}
- 定义:Node[] tab; Node p; int n, i;
- [if] HashMap数组为空或者长度为0
- 进行HashMap的初始化resize(),并获取数组的长度:table.length
- [if] 通过index = (n - 1) & hash,定位到数据元素为空。——未发生hash碰撞。
- 直接在当前index生成一个Node:newNode(hash, key, value, null)
- [else] 代表发生hash碰撞了。进一步判断(先进行key判断,后进行value判断):
- 定义:Node e; K k。
- [if] 当前索引对应的Node,key的hash值和key本身与要添加的都相同。
- 复制一份当前Node,后续进行value判断。
- [else if] 如果当前索引的Node属于TreeNode(红黑树)
- 通过方法:((TreeNode)p).putTreeVal(this, tab, hash, key, value),添加红黑树节点。
- [else] 链表情况:循环遍历,中断条件为内部判断逻辑。
- [if] 节点Node的next(e = p.next)元素为空:最后一个
- 生成一个新的Node,并赋予p.next
- 如果循环遍历次数超过 TREEIFY_THRESHOLD 8个,则则进行树的转换:treeifyBin(tab, hash)。
- 终断循环。
- [if] key的hash值和key和key本身与要添加的都相同。
- 终断循环。
- 循环到最后,获取最后一个Node——p = e
- [if] 存在key对应的节点映射Node,即key冲突。
- [if] onlyIfAbsent = true 获取原有value为空
- 覆盖原有value。
- 进行预留后置处理:afterNodeAccess(e);——LinkedHashMap使用
- [return] 原有value(oldValue)——即如果放入的key相同,则返回被覆盖的value。
- 增加操作次数:++modCount
- [if] (++size > threshold) 超过了扩容的阈值
- 进行扩容:resize()。
- 进行预留后置处理:afterNodeInsertion(evict);——LinkedHashMap使用(重写)。
- [return] null
- 列表扩容方法 resize
该方法整体分为三部分:
- 初始化扩容
- 寻找扩容后数组的大小以及新的扩容阈值
- 非初始化时将旧的table复制到新的table中
关键核心点:
JDK1.8 中扩容后,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap。
final Node[] resize() {}
- 复制一份当前HashMap的table:Node[] oldTab = table;
- 确定oldTab的容量和扩容阈值,定义newTab的容量和阈值为0。
- [if] 旧容量大于0,实际上旧扩容阈值也大于0。
- [if] 旧容量大于最大值:MAXIMUM_CAPACITY
- 把扩容阈值threshold设置为最大值:Integer.MAX_VALUE
- 直接返回原来表。不进行扩容。
- [else if] 旧容量X2后,小于最大值:MAXIMUM_CAPACITY 且旧容量大于等于 DEFAULT_INITIAL_CAPACITY
- 新容量newThr = oldThr << 1, 扩容两倍 。
- [else if] 旧扩容阈值大于0,即旧容量等于0:代表正在初始化 。
- 新扩容阈值 = 旧扩容阈值
- [else] 这个情况代表 未初始化
- 设置newCap = DEFAULT_INITIAL_CAPACITY
- newThr =DEFAULT_LOAD_FACTOR X DEFAULT_INITIAL_CAPACITY
- [if] 新扩容阈值 newThr == 0:只有一种情况:loadFactor人为设置很高
- 计算新容量的扩容阈值。
- 如果新容量扩容阈值大于最大值 MAXIMUM_CAPACITY 则设置newThr = Integer.MAX_VALUE,否则设置为实际计算的。
- 当前threshold = newThr。
- 进行新旧表复制 :新建表newTab = (Node[])new Node[newCap];
- 当前table = newTab
- [if] oldTable != null——空表不需要复制
- 循环遍历旧表容量值
- [if] 循环的当前节点不为空,赋予新Node e = oldTab[index]
- 旧表当前值(oldTab[index])置为空。——释放旧表空间
- [if] e节点没有next节点。——即当前节点为数组节点
- 复制旧表数据到新表索引(e.hash & (newCap - 1))位置。——位置一致。
- [else if] 旧表当前节点是红黑树
- 使用方法:((TreeNode)e).split(this, newTab, j, oldCap),进一步确认一步确定树中节点位于新数组中的位置。
- [else] 链表结构
- 定义链表的高位头尾(loHead/loTail)和低位节点(hiHead/hiTail)
- 循环do-while,直到next为空。——遍历旧链表。
定义next = e.next
[if] (e.hash & oldCap) == 0判断,属于低位区域,在低位的头尾赋值e。
[else] 属于高位区域,在高位的头尾赋值e。
- 如果低位尾不为空:loTail != null
- 设置loTail.next = null
- 将低位链表存放在 原index(j) 处。
- 如果高位尾部不为空:hiTail != null
- hiTail.next = null;
- 将高位链表存放在 新index(j + oldCap) 处。
- 返回新表newTab
添加map实例 putMapEntries
批量元素添加,主要用于:putAll()/HashMap(Map extends K, ? extends V> m)构造方法/clone()复制方法
核心流程:
- 判断是否初始化
- 判断是否需要扩容
- 遍历待添加map,调用putVal接口一一添加。
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {}
- 获取入参Map的size:int s = m.size();
- 只有入参Map的size>0,才继续操作。
- [if] 当前map 未初始化:table = null。
- 待扩容size/loadFactor +1,与最大值判断,取得预估容量t。
- 如果ft大于现有hashMap的threshold,则使用方法tableSizeFor(t)得到最接近t且为2^n的容量。
- [else if] 待添加的hashMap容量大于扩容阈值:threshold。
- 进行扩容:resize()。
- 遍历待添加的hashMap。
- 获取对应的key和value。
- 使用方法: putVal(hash(key), key, value, false, evict),进行添加。
查询节点 getNode
根据key的hash值获取对应的Node,主要用于get(Object key)/containsKey(Object key)/replace(K key, V oldValue, V newValue) 等方法。
这些方法最终获取的是Node.value。
核心流程:
- 通过经过hash后的key,以获取数组的形式获取Node节点:tab[(n - 1) & hash]
- 如果Node不为空,判断key值是否一样
- 如果对应Node的next不为空,是否属于TreeNode,通过getTreeNode获取对应Node。
- 如果是链表,循环获取对应的Node,判断key是否一致,返回对应Node。
final Node getNode(int hash, Object key) {}
- [if] 当前hashMap,table !=null 且 size>0 且tab[(n - 1) & hash]) != null,数组中有对应的Node。
- [if] hash值和key值相等。
- 直接返回对应的Node
- [if] (e = first.next) != null,即Node位置为链表或者TreeNode。
- [if] Node节点属于TreeNode
- 返回通过方法.getTreeNode(hash, key),获取到的Node——从TreeNode的父级节点开始find节点。
- [else] 链表结构,循环遍历链表,直到next == null。
- 返回hash和key值一样的Node节点。
- 返回null;数组为空,不需要删除。
删除节点 removeNode
这个方法有多个入参,决定value是否一致才删除,或者删除后TreeNode是否在删除后“平衡移动”
核心流程:
final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {}
- 通过key找到对应的Node
- 如果找得到(value是否一致)删除对应的Node
- 进行其他移动(TreeNode),—size
- 当前hashMap,table !=null 且 size>0 且tab[(n - 1) & hash]) != null,数组中有对应的Node。同getNode的方法
- [if] hash一致且key一致,说明是数组节点。
- 提取对应的Node,后续使用 :node = p 。
- [else if] 当前Node有next节点,说明是链表或红黑树:
- [if] Node节点属于TreeNode
- 通过方法.getTreeNode(hash, key),获取到的Node——从TreeNode的父级节点开始find节点。
- 提取对应的Node,后续使用 :node。
- [else] 链表结构,循环遍历链表,直到next == null。
- 得到hash和key值一样的Node节点。
- 提取对应的Node,后续使用 :node = e 。
- 循环遍历到最后一个元素
- 提取对应的Node,后续使用 :p = e 。确认匹配不到对应的节点!
- [if] node != null,如果需要严格按照value值是否一致,则进行匹配。条件成立说明能找到要删除的节点:
- [if] node属于TreeNode
- 通过方法:removeTreeNode(this, tab, movable)删除对应的node节点,并根据参数:movable 确认是否移动Tree节点。
- [else if] node == p, 属于数组节点:
- 直接把当前索引Node指定为下一个:tab[index] = node.next。
- [else] 属于链表:
- 直接把当前节点的next指定为定位到的节点的next,链表缩减 p.next = node.next。
- ++modCount, --size
- 进行预留后置处理:afterNodeRemoval(node);——LinkedHashMap使用(重写)。
- 返回null;数组为空,不需要删除。
删除节点 clear
删除HashMap的所有节点。源码比较简单。
核心流程:
- 确认表不为空,为空不处理。
- 把size置为零。
- 循环数组:table[index] = null。暴力方式,Node如果是Tree或者链表无引用后被回收(为何不单独判断红黑树或者链表?)