HashMap基础概念
HashMap简述
哈希散列表存储key-value键值对,key是唯一的,value可以存储null值,不能保证映射的顺序,可动态扩容,底层数据结构是数组+链表+红黑树(红黑树jdk1.8才有引入)
散列表的特点
整合了数组和链表,结合了两者的优势:数组的快速索引,链表的动态扩容
数组的优/劣势
优势:数组的内存空间是连续的,每一块空间都是一样大小,通过索引可以快速访问指定位置
劣势:在java中数组的内存申请完(初始化)之后大小是固定的,如果有新的元素进来发现内存不足时需要重新创建一个新的数组进行扩容,将之前的数据拷贝到新的数组中然后再加入新的元素,这样不太灵活,拷贝数据的过程也是比较浪费性能的
链表的优/劣势
单向链表从链表结构上来看它不是一个连续的内存空间,而是当前节点保留一个引用指向下一个节点的
优势:新插入的元素,是不关心空间的,不受初始内存的影响,新增元素时直接拆开一个链往里塞,或者添加到尾部或头部
劣势:一般只保留头部节点,假设你要找的元素是最后一个,只能从一个一个节点往下找,不能通过索引直接获取指定位置的元素
什么是哈希(hash)
把任何长度的输入,通过hash算法变为固定长度的输出,这个映射的规则对应的就是Hash算法,而原始数据映射后的二进制串就是哈希值
哈希(hash)的特点
1、hash值不可逆,也就是不可反向推出原始数据
2、输入的数据发生微小变化也会得到不同的Hash值
3、hash算法执行效率高,较长的文本也能快速计算出Hash值
4、hash算法的冲突概率小(由于hash的原理是将输入空间的值映射成hash空间内,而hash空间小于输入空间。根据抽屉原理,一定会存在不同的输入映射成相同的输出的情况)
HashMap存储结构
数组+链表+红黑树
数组初始长度为16,在没有发生hash碰撞时按正常往数组中放,发生碰撞之后在当前桶位就会形成一个链表,当链表结构长度超过8且当前Hash结构中元素超过64时当前链表升级为红黑树
什么是hash碰撞
每次往HashMap中加入的元素的key计算出来的hash值跟之前的相同,然后通过寻址算法得到的桶位发现已经有数据了,此时当前桶位就会形成链表;
hash碰撞带来的问题
hash碰撞会导致链化,在根据key来get数据时,通过寻址算大得到桶位(数组下标),由于碰撞次数多导致链表变长,需要挨个去比较node的key是否跟你要获取的相同,使得整个查询效率变低,理想状态hash表的查询效率是O(1),如果链化严重查询效率就退化成O(n)了。
HashMap的put流程
1、获取存储HashMap中key的hash值
2、经过hash扰动函数将hash边的更散列
3、构造一个出Node对象
4、通过寻址算法得到桶位(数组下标)存入Node
HashMap扩容原理
是一个以空间换时间的思想,当HashMap中存储的数据超过了扩容阈值(扩容阈值=数组初始容量*负载因子)就会触发扩容,数组长度变长后意味着链表就会变短,其目的也是为了提升查询效率。
HashMap源码
HashMap核心属性分析
//核心属性
DEFAULT_INITIAL_CAPACITY:默认的table容量大小,(1<<4)为16
MAXIMUM_CAPACITY:默认的最大容量 (1<<30)
DEFAULT_LOAD_FACTOR:默认的负载系数 0.75
TREEIFY_THRESHOLD:树化(红黑树)阈值,默认值8
UNTREEIFY_THRESHOLD:树(红黑树)降级成为链表的阈值,默认值6
MIN_TREEIFY_CAPACITY:树化(红黑树)的另一个参数,只要哈希表中的所有元素超过64个并且当前桶位的链表超过8才会允许树化,默认值64
//字段含义
Node<K,V>[] table:哈希表,该表在首次使用时初始化,并根据需要调整大小。分配时,长度始终是2的次方。
int size:哈希表中元素个数
int modCount:当前哈希表的结构修改次数
int threshold:扩容阈值,当你的哈希表中的元素超过阈值时触发扩容,计算公式:threshold = capacity * load factory
float loadFactor:负载因子,默认0.75
构造方法分析
/**
* 构造一个具有指定初始容量和负载因子的空HashMap
*
* @param initialCapacity 初始容量
* @param loadFactor 负载因子,决定哈希表中在元素达到多少时进行扩容
* @throws IllegalArgumentException 当初始容量和负载因子不满足要求时抛出的异常
*/
public HashMap(int initialCapacity, float loadFactor) {
//做了一些数据合法性校验
//初始容量必须大于0且不能超过默认的最大容量
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能小月0 or 不能为空
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
tableSizeFor方法作用:返回一个大于等于当前容量的数字,并且这个数字一定是2的次方数
/**
* Returns a power of two size for the given target capacity.
* 返回给定目标容量的两倍幂(2次方)
*/
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;
}
HashMap的put方法分析
其底层就是套娃 了putVal方法
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//中文释义:
//计算 key.hashCode() 并将哈希的较高位传播(XOR)到较低位。
//由于该表使用二次幂掩码,因此仅在当前掩码之上位变化的散列集将始终发生冲突。 (已知的例子是在小表中保存连续整数的 Float 键集。)
//因此,我们应用了一种变换,将高位的影响向下传播。在位扩展的速度、实用性和质量之间存在折衷。
//因为许多常见的散列集已经合理分布(所以不要从传播中受益),并且因为我们使用树来处理 bin 中的大量冲突,我们只是以最便宜的方式对一些移位的位进行异或,以减少系统损失,以及合并最高位的影响,否则由于表边界,这些最高位将永远不会用于索引计算。
//作用:让key的hash值的高16位也参与运算
//亦或算法:相同则返回0,不同则返回1
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab: 引用了当前HashMap的散列表
//p: 表示当前散列表的元素
//n: 表示散列表的长度
//i: 表示路由寻址的结果(也可以说是数组的下标)
Node<K,V>[] tab; Node<K,V> p; int n, i;
//延迟初始化逻辑,在第一次插入数据也就是调用putVal的时候初始化HashMap对象中最耗费内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//最简单的一种情况:寻址找到的桶位刚好是null,此时直接将构建好的node对象扔进散列表就可以了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//e:表示一个临时元素
//k:表示临时元素的一个key
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 {
//链表的情况,而且是链表的头元素与我们要插入的key不一致
for (int binCount = 0; ; ++binCount) {
//条件成立的话,说明迭代到了最后一个元素,也没有找到与你要插入的key一致的node
//说明需要加入到当前链表的末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//条件成立的话,说明当前链表的长度已经达到树化的标准了,需要进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//树化的操作
treeifyBin(tab, hash);
break;
}
//条件成立的话,说明找到了相同key的node元素,需要替换
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不等于null,条件成立的话,说明找到了相同key的node元素,需要进行替换操作,并返回老的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//表示散列表结构修改的次数,替换node元素的value时不计数
++modCount;
//++size: 插入新元素加1,如果自增后的值大于扩容阈值时则触发扩容
if (++size > threshold)
//扩容逻辑
resize();
afterNodeInsertion(evict);
return null;
}
HashMap的resize扩容方法分析(核心)
为什么需要扩容?
为了解决哈希冲突导致的链化影响查询效率的问题,扩容后可以缓解该问题。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
//oldTab:表示扩容前的哈希表table
Node<K,V>[] oldTab = table;
//oldCap:表示哈希表扩容之前的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:表示扩容之前的扩容阈值,也就是触发本次扩容的阈值
int oldThr = threshold;
//newCap:扩容之后的哈希表table数组大小(扩容后的容量)
//newThr:扩容之后,也就是下一次触发扩容的阈值
int newCap, newThr = 0;
//条件成立的话,说明HashMap中的散列表已经初始化过了,这是一次正常的扩容
if (oldCap > 0) {
//扩容之前的数组已经达到了最大容量,则不扩容,直接设置下次扩容的条件(阈值)为 int 的最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//oldCap左移一位实现数值翻倍,并赋值给newCap,newCap小于最大限制 且 扩容前的容量(oldCap)小于等于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//满足条件,下一次扩容条件翻倍
newThr = oldThr << 1; // double threshold
}
//oldCap==0的情况,说明hashMap的散列表是null
//1. new HashMap(initCap, loadFactory);
//2. new HashMap(initCap);
//3. new HashMap(map); 并且map是有数据的
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//oldCap==0,oldThr==0 的情况
// new HashMap();
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//newThr为零时,使用newCap和loadFactor计算出newThr
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;
//说明扩容之前table中有数据
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//当前node节点
Node<K,V> e;
//说明当前桶位有数据,但是具体数据是 单个数据 还是 链表 还是 红黑树 需要进行下一步判断
if ((e = oldTab[j]) != null) {
//方便JVM GC时回收内存
oldTab[j] = null;
if (e.next == null)
//当前桶位只有一个元素,没有发生任何hash碰撞,这种情况直接计算出当前元素应存在 新数组的位置 然后扔进去就可以了
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元素
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的get方法分析
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
//tab:引用当前hashMap的散列表
//first:桶位中的头元素
//e:临时node元素
//n:table数组长度
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方法分析
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* Implements Map.remove and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab:引用的当前散列表
//p:当前node元素
//n:引用的当前散列表数组长度
//index:表示寻址结果
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:查找到的结果
//e:当前Node的下一个元素
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 {
//链表的情况
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//删除逻辑
//判断node不为空,说明通过key已经找到要删除的元素了
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//第一种情况:node阶段是树节点,说明需要树节点的移除操作
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//第二种情况:桶位元素即为查找结果,直接将该元素的下一个元素放置桶位中(可能有值也可能是null)
else if (node == p)
tab[index] = node.next;
//第三种情况:将当前元素p的下一个元素 设置成 要删除的node元素的下一个元素
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
HashMap的replace方法分析
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}