和HashMap谈对象
”双肩包“、“格子衫”、“秃秃头”、“单身狗”,作为一名伟大而光荣的程序员,为啥给我贴这么多标签?宝宝心里苦呀!别人笑我太疯癫,我笑别人看不穿。我微微一笑,心中轻蔑的想:“无知的人类,其他我都忍了,敢说我们单身狗,知道我们每天创建多少个对象吗? 什么?不服气,今天就给你们介绍介绍魔鬼身材,温柔贤惠的HashMap”。
一、HashMap的底层数据结构
先给你瞟一眼HashMap这个妮儿的样子,唉!唉!唉!你别抢我的照片呀!
主要有数组、链表和红黑树组成,数组中每个元素称为一个桶,每个桶中通过链表的方式存放多个节点,当链表的长度到达一定的阈值后,链表就会转换为红黑树。
可别觉得花里胡哨的,我们的HashMap可不是花瓶,虽然没吃一个Key-Value都会变胖(链表长度变长),但是我能利用他们变成大长腿(HashMap的resize,数组长度边长),并且让横向生长的肉肉长的慢一点(链表的树化)。
二、HashMap的属性
先给你说说HashMap这个妮儿的长相(属性),“肤白貌美大长腿,腰细齿白樱桃嘴。勤俭持家解人意,深入了解不后悔。”
//这个数组就是用来存储键值对的表。每一个元素都是Node类型。Node类的定义如下:
transient Node<K,V>[] table;
//hashMap的节点,用来存储键值对
class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key的hash值
final K key; //键
V value; //值
Node<K,V> next; //下一个节点的引用
}
//你可能轻蔑的一笑说,就这还是美女呢?别着急呀,这些才是皮毛,皮毛懂吗?
//默认初始容量。就是默认table数组的长度。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量。table数组的最大长度。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 看到没!看到没!她身高能长到1 << 30 cm,(悄悄告诉你,吃的多才长这么高)
//默认负载因子。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶树化的阈值
static final int TREEIFY_THRESHOLD = 8;
//桶解除树化的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//树化的最小容量。即table的长度达到64并且桶中的节点数量大于8才会进行树化
static final int MIN_TREEIFY_CAPACITY = 64;
//下一次resize的阈值(capacity * load factor).如果尚未为分配数组table,那么这个值表示初始容量
int threshold;
//负载因子
final float loadFactor;
//懵了懵了!怎么那么多属性呀这哪能分得清呀,然我们结合HashMap的秘籍来更深入的了解她
三、HashMap的重要方法
3.1 HashMap的构造函数
知道HashMap这个妮儿是怎么来的吗?你鄙夷的看了我一眼慢慢说:“羞羞的事情之后,形成受精卵,然后........”。
NO! No! No! 我摇着手指。什么羞羞的事,什么受精卵,都是通过构造函数new出来的。
//无参构造函数。只为负载因子赋了个默认值,其他什么都没有做。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//参数为初始化容量。然后调用最后一个构造函数。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//参数为初始容量和负载因子
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);
}
//通过给出的容量计算table的长度。这个值只能是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;
}
3.2 HashMap的put方法
HashMap这个妞最重要方法了,key-value键值对是怎么存的,什么时候进行resize,什么时候进行链表到树的转换都能在这个方法中看到。
//传入key-value,这个经常使用
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 通过hashcode的高16位和低16位异或运算计算得到hash值,增加hash值的差异性。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//参数:
/*
hash:key的hash值
key: key
value: value
onlyIfAbsent: 如果为true则不改变已存在的值
evict:如果是false,则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;
//前面的构造函数并没有对table数组进行初始化,在第一次put的时候table需要进行初始化,
//这里会调用resize()进行初始化,这个待会儿再介绍
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过(n - 1) & hash算出再哪个桶中。因为数组的长度为2的整数次方通过&运算计算出位置。
//如果这个值是null,直接放入这个数组中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//这个里面是key的hash值冲突的情况,桶中可能已经存在其他元素。
//变量e表示已经存在相同的key的节点。
Node<K,V> e; K k;
//判断第一个节点是不是和要插入的key相同。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//如果p是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);
//如果到达的树化的阈值,则执行treeifyBin进行树化。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果已经存在要插入的key就结束了。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e != null 说明存在和要插入的key相同的节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
//通过onlyIfAbsent判断是否要覆盖已经存在的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果size到达了rehash的阈值则执行resize方法
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
我们通过一个流程图梳理一下整个put的过程。
3.3 HashMap的resize方法
分析完HashMap的整体过程后,我们接下来就详细的分析一下resize方法的代码,看看HashMap是如何进行自动扩容的。
//哇。。。。 好长呀! 别着急,耐心看下去
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;
}
//新容量就是将oldCap*2
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
//初始阈值是0的时候使用默认值
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"})
//初始化table,如果第一次插入key-value,则执行到这里就初始化结束,返回结果了,下面的if中代码不会执行。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//这个if中是真正的扩容,将老table中的数据迁移到新的table中
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)
//桶中只有一个节点,重新计算hash值放入新的table中
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果是树节点则对数节点进行rehash
((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;
//这个循环中就是遍历老的链表,然后重新计算hash值后移动到新的table中
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//这里表示这些节点不需要迁移,重新计算后仍然处于这个桶中
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//这里表示需要迁移到j + oldCap位置的桶中的节点
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;
}
}
}
}
}
//返回新table
return newTab;
}
建议大家可以对照着源码,按照注释来慢慢分析resize的过程,相信你们聪明的小脑袋一定能够读懂这段逻辑。
3.4 HashMap的get方法
接下来可以放轻松了,介绍一下简单的get方法,看看历尽千辛万苦吃进去的的key-value,怎么才能吐出来。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//hash:key的hash值
//key:key
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) {
//计算出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);
//遍历链表查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//没有查到则返回null
return null;
}
//简单吧,这就是小意思
四、HashMap不是线程安全的
爱了!爱了!HashMap这个妞儿这么好呀!先别着急呀!俗话说:“金无足赤,人无完人”。HashMap也是有不足之处的,那就是HashMap不是线程安全的,因此在并发场景下,可能会出现问题哦!那么在多线程情况下会出现什么情况呢?
1、多线程put操作后,get操作导致死循环。(1.8 通过将头插法改成尾插法,解决了这个问题)
2、多线程put非NULL元素后,get操作得到NULL值。
3、多线程put操作,导致元素丢失。
我总结了以上三个线程不安全的场景,如果你有补充欢迎留言。
五、JDK 1.7 和 1.8 HashMap的区别
JDK1.8 对HashMap进行了优化,提供更好的性能和安全性。主要有一下区别:
- 存储结构不同。1.7是数组+链表的存储结构;1.8是数组+链表+红黑树的形式存储元素。因为当链表的长度过长的时候,查询的效率就直线下降,所以在JDK1.8之后将其设计为当链表的长度达到一定的阈值的时候,就会将链表结构转换为红黑树结构,红黑树是一种自平衡的二叉搜索树,提高查询效率。
- 插入数据的方式不同。1.7及之前采用的是链表头部插入数据;1.8及之后采用的是链表尾部插入数据;扩容后转换数据不需要遍历到链表的尾部再插入,最近添加的元素可能马上就要被获取,头插的方式只需要遍历到链表的头部就能匹配到了。扩容后链表可能会倒序,并发扩容可能会产生循环链。
- Hash运算不同。
- 扩容方式不同。1.7及之前首先检查是否需要进行扩容,再插入数据(扩容为原来的两倍);1.8及之后首先插入数据,再检查是否需要扩容,并优化了扩容算法。
六、结语
相信看完这篇文章之后能对你认识HashMap这个妮儿有帮助,只能帮到这里了,能不能成就靠你自己多多的去了解了,要是有帮助的话别忘了点个关注哈!