1,数据结构
在jdk1.8之前HashMap采用数组+链表的结构,而在1.8的时候采用数组+链表+红黑树的结构,为什么要采用数组+链表+红黑树呢
- 数组查询效率快
- 链表是为了解决数组同一位置的冲突,还有一个就是删除,添加快
- 红黑树是在链表长度太大时,将链表改成黑红数可以大大提高效率
数据结构大概就是这个样子
2,构造器
//第一个构造器:
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,不能为非数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 初始化填充因子
this.loadFactor = loadFactor;
// 初始化threshold大小
this.threshold = tableSizeFor(initialCapacity);
}
//第二个构造器
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//第三个构造器
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
第一个构造器有两个参数,一个是初始容量,一个是负载因子
第二个构造器只有一个初始容量的参数
第三个不需要参数,全部使用默认的
通过上面三个构造器,可以看出有这么几个参数是比较重要的:
/**
*数组的初始容量
*/
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;
/**
*把链表转换为红黑树的阀值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 把红黑树还原成链表的阀值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
*最小树形化的容量阀值
*/
static final int MIN_TREEIFY_CAPACITY = 64;
看完构造器再来看一下常用的添加,查询,删除操作
3,添加(put):
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
第一步调用putVal(),里面hash(key)比较重要
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里主要用了一个异或运算,将key的hashCode值的高16位和低16位进行异或,目的就是为了解决Hash冲突,因为数组的默认容量为16,转换为二进制是10000,完整的hash是32位,这里只有五位,使用高16位和低16位异或一下就能充分把32位利用起来了,而且这个异或运算在效率上比起原来的取模运算效率更快
/**
* 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) {
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;
}
初看一下这代码还挺吓人的,不过仔细一看,不就是一堆if,else吗,容我慢慢分析
1:五个变量
int hash //哈希值,用于计算下标
K key //键值
V value //需要存储的Value值
boolean onlyIfAbsent//是否替换原来的值,false为替换,true为不替换
boolean evict //是否为创建模式
2:初始化桶
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
这里通过判断tab来决定是否进行初始化,而tab又是table赋值而来,通过前面的构造器我们并没有看见有对table初始化,所以这里会进入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() {
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;
}
本以为只是一个初始化,可看这代码不像啊,这也太复杂了吧,看来不仅仅是初始化这么简单啊,是的,其实这个方法的作用是扩容,初始化也当作扩容的一部分,别看他这么复杂,其实我们这次进来执行的代码就几句
newCap = DEFAULT_INITIAL_CAPACITY;//桶的初始容量
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//扩容阀值,达到这个值就需要扩大桶的容量了,默认为0.75 * 16 = 12
下面才正式开始添加的步骤,之前都是准备
通过上面的数据结构我们可以看出有三种结构,分别是数组,链表,红黑树,在添加时我们把这三种结构分为了三种情况
第一种:数组
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
第一种最简单,通过hash值计算出下标,然后判断桶中对应下标是否有值,当为null时则添加进去
第二种,红黑树
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);
首先用桶下标对应值的key和现在需要添加的新key用equals进行比较,如果相同,则覆盖之前值,
判断桶下标存的值是否为红黑树,如果是则进行红黑树的节点添加
第三种:链表
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;
}
}
循环链表的元素,通过next取得下一个元素,如果下一个元素为null,则把当前要添加的元素添加进去,添加过后判断链表元素是否达到8个,如果达到则把链表转换为红黑树,这里还有一个判断是,如果链表里面的key和新添加的key相同则直接退出,这里也是用equals做比较
最后一步,采用尾插法插入节点保存数据,并返回老数据,最后判断添加过后的容量大小是否达到扩容的阀值,如果达到则进行扩容
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);
再来看一下扩容操作
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;
}
//如果扩容后小于最大值,并且大于默认值16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 扩容为原来的两倍
}
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"})
//创建一个新hash桶,将原来的hash桶传进去
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//用循环把原来hash桶的数据,插入到新hash桶里面去
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//原来hash桶数据只有一个元素,不是链表时
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//原来hash桶数据是一个红黑树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//原来hash桶数据一个链表
else { // preserve order
//把链表的值重新hash分布一遍
Node<K,V> loHead = null, loTail = null;//重新分布的索引为原索引
Node<K,V> hiHead = null, hiTail = null;//重新分布的索引为原索引+原hash桶容量大小值
Node<K,V> next;
//这里又使用一个循环来
do {
next = e.next;
//如果节点的hash于原来的桶容量大小等于0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//如果e的hash值与老表的容量进行与运算为1,
//则扩容后的索引位置为:老表的索引位置+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点),
//则将最后一个节点的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点),
//则将最后 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
特别需要注意,在扩容的时候又一个判断来确定扩容以后元素的存放位置 if ((e.hash & oldCap) == 0) . 为什么要与原来的数组长度来做与运算呢 ?这其实也是为了让元素更加均匀的分布,就是用元素的 hash 值 与老的数组长度的与运算,当不等于0的时候就进行将元素迁移到 +oldCap 的位置上。这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。
4获取(get)
在看完添加过程之后再来看获取,这时就很舒服了,上代码
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
和添加时一样,先把key使用hash算法hash一下,存的时候都是根据这个值存的,取的时候那肯定也需要这个值
下面看一下获取的主要过程
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//table不为null并且已经初始化了
if ((tab = table) != null && (n = tab.length) > 0 &&
//根据hash算出下标,并且桶下标所对应的值不为null
(first = tab[(n - 1) & hash]) != null) {
//如果第一个节点的hash和要取出的hash值相同,则返回这个值
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);
//不为红黑树时,就为链表,使用循环判断链表元素的key和要取出的key是否相同,使用equals来进行比较
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
判断表或key是否是null,如果是直接返回null
判断索引处第一个key与传入key是否相等,如果相等直接返回
如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值
如果不是树,就遍历链表查找
5删除(remove)
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
老规矩,还是先计算一下hash值,然后交给另外一个方法
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//先看一下桶是否有初始化
if ((tab = table) != null && (n = tab.length) > 0 &&
//再看一下桶里面有木有这个经过hash算出来的下标所对应的值
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//如果第一个节点的hash和要取出的hash值相同
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);
}
}
//节点不为null
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//节点为红黑树
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//节点为为普通元素
else if (node == p)
tab[index] = node.next;
//节点为链表
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
删除操作,先计算指定key的hash值,然后计算出table中的存储位置,判断当前位置是否Entry实体存在,如果没有直接返回,若当前位置有Entry实体存在,则判断节点的状态,如果为红黑树,则使用红黑树的方法进行删除,如果为普通元素则把下一个元素赋值给当前元素,如果为链表则把他的赋值给下一个,使要删除的这个元素失去引用
6总结:
- hash函数使用了hashCode的高16位异或低16,降低了hash冲突
- 使用 (n - 1) & hash 计算桶索引,提高了效率
- 使用链表来解决hash冲突,当链表长度有八个时,把链表转换为红黑树,删除后如果红黑树的元素为6还原成链表
- 插入时采用尾插法,解决了死循环的问题
- 每次扩容为原来的2倍,扩容后会将原来的元素进行重新hash分布,分布的位置为原来的索引或原来的索引加上原来桶的容量大小(table的长度始终为 2 的 n 次方;索引位置的计算方法为 “(table.length - 1) & hash”。)
参考文章:
- https://www.cnblogs.com/xiaoxi/p/7233201.html
- https://www.cnblogs.com/wuzhenzhao/p/13199350.html
- https://blog.csdn.net/qq_41345773/article/details/92066554