HashMap介绍
HashMap是基于hash表的map的实现,使用key-value形式存储键值对,并允许使用 null 值和 null 键,但是key只能有一个为null. Map不保证映射的顺序,其内部是根据hash值去模运算去排列的。HashMap内部使用entry数组作为存储的介质.
本文是基于Java8版本写的,因为Java8对HashMap有较大的改变,采用数组+链表+红黑树
方式来记录键值对. 而且Java8还重写了resize方法,Java8之前很有可能造成扩容时,链表的循环问题.
源码解读
本文中的代码比较多,大多数的说明都在注释上体现了,所以可能文字上表述的不是很多,本文也尽可能介绍了一些java8中的新方法.需要注意的是其中很多地方都进行了修改和补充,看完本篇文章一定和之前看的Java7 HashMap有非常多的不同之处,也可以思考一下为什么Java8要做这么多改变.
Node介绍
Node是map的接口中的内部接口Map.Entry的实现类,用于存储HashMap中键值对的对象,是HashMap中非常重要的一个内部类,随便提一下,HashMap中有非常多的内部类,本文没做过多的介绍,读者可以自己翻看一下源码,因为篇幅实在太长了…在这里就简单的讲一下,大部分的内部类都是用于集合操作的,如keySet
,values
,entrySet
等方法.
内部组成
static class Node<K,V> implements Map.Entry<K,V> {
//key是不可变的
final K key;
//value
V value;
//指向下一个entry对象,具体作用后面介绍
Node<K,V> next;
//hash值
int hash;
}
HashMap组成
HashMap主要参数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始容量
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子,相当于只有多少可用
transient Node<K,V>[] table;//要存储值的hash表
transient int size; //实际大小
int threshold; //阈值
final float loadFactor;//负载因子
transient int modCount; //记录操作次数的
相比之前版本的HashMap,Java8更钟爱与位操作. >> 表示逻辑右移 >>> 表示无符号逻辑右移.高位补零
HashMap构造方法
//为空时候使用默认分配的大小16,负载因子0.75f,默认的容量为12,当size>threshold默认容量时候就会去扩容
public HashMap() {
this.loadFactor = 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);
}
HashMap方法
本文主要挑选几个比较常用且重要的方法进行分析.因为这写方法已经涵盖了大部分的方法调用关系.
hash方法 使用hashcode 异或
hashcode右移16位 得到hash值,等同于高位都取零 随带提一句,int类型的hashcode就是本身.
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
put方法解析 put方法,是HashMap中非常重要的方法,其中牵涉到了非常多的知识点,需要仔细的阅读.
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* onlyIfAbsent 是否替换,为true时,如果存在值则替换
* evict 主要用于LinkedHashMap移除最近未使用的节点
*/
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;
//如果不存在,直接put
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//根据条件获取不同的node节点,有可能tab[i]就是存在的元素
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;
//用于linkedHashMap,本文不做介绍
afterNodeAccess(e);
return oldValue;
}
}
//添加成功,判断是否要扩容
++modCount;
if (++size > threshold)
resize();
//用于linkedHashMap
afterNodeInsertion(evict);
return null;
}
下面详细介绍一下扩容的方法.看起来非常庞大.其实逻辑不复杂
/**
* 有几种情况
* 1.当为空的时候,也就是没有初始化的时候
* 2.当到达最大值时候
* 3.普通扩容时候
*/
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) {
//大于2<<30 最大容量设置为2<<32 - 1
if (oldCap >= MAXIMUM_CAPACITY) {
//但是不移动.,没有空间移动
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果属于正常范围的扩容 容量 x2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//用户自己设定了初始化大小
else if (oldThr > 0) {
newCap = oldThr;
}
//如果没使用,使用默认值初始化
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//用户自定义了map的初始化操作
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) {
//遍历map的数据
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);
//如果没超过8个 是链表
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//此处的操作是这样子的 因为是扩容一倍的操作,所以与旧的容量进行与操作后只有两个值0 和 1
//如果是0就位置不变,如果是1就移动n+old的位置,
//个人认为这么做的好处是:
/**
* 1.不会像之前1.7发生循环依赖的问题
* 2.从概率的角度上来看可以均匀分配,(一般来说高位和低位比例差不多)
* 3.提高效率
*/
do {
next = e.next;
//如果和旧容量位运算后的值是0,记得当前节点和存放在链表的尾部
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);
//为0的还是存放在当前位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//为1的就放在扩容的j + oldCap那边去
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
因为不像Java8之前的HashMap有初始化操作,此处选择将初始化和扩容放在了一起,并且又增加了红黑树的概念,所以导致整个方法的判断次数非常多,也是这个方法比较庞大的主要原因.
值得一体的是,在扩容后重新计算位置的时候,对链表进行优化,有兴趣可以搜索一下HashMap导致cpu百分之百的问题 而在Java中通过巧妙的进行&操作,然后获得高位是为0还是1.最终移动的位置就是低位的链表留在原地,高位的放在index+oldsize的地方就可以了,不用为每一个元素计算hash值,然后移动到对应的位置,再判断是否是链表,是否需要转换成树的操作.如下所示.
hashcode: 1111111111111101212
oldcap: 0000000000000010000
很容易知道这个&操作之后就是为0,因为oldcap都是2的倍数,只有高位为1,所以通过&确认高位要比%取余高效. 此时在看一下上面的扩容操作也许就更清晰了.
下面介绍一些newNode方法.就是新建一个节点.可以思考一下为什么要把newNode抽离出来?文章末尾给答案.
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
添加节点到红黑树的方法是Java8中新添加的,需要满足链表的长度到8,才会转换成红黑树,其主要目的是防止某个下标处的链表太长,导致在找到的时候速度很慢,下面看一下实现
//尝试着往树节点添加值
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//找到根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//存在的话直接返回,用于替换
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//判断节点类型是否相同,不相同
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//没有搜索过,搜索子节点,搜过了说明没有就跳过.
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
//去子节点去找
if (((ch = p.left) != null &&(q = ch.find(h, k, kc)) != null) ||((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//对比hash值,决定查找的方向
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
//找到子节点为空,就可以加进去,设置层级关系
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
这里简单的梳理一下流程. 1.从根节点查找,找到了返回,如果没找到,找字节点 2.判断往哪个方向去查找 3.如果不存在,在子节点末端添加新节点
下面再看一下树的split方法,主要是扩容操作,重新结算位置需要分裂树,之前讲过,扩容会根据和旧map容量进行&操作,移动高位为1的节点.并且验证新的节点列表是否需要重新转换成链表的形式.
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// 设置记录高低位的node,和链表一样都是计算高位是0还是1
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
//还是和旧的容量做位运算,为0的放在lo中
if ((e.hash & bit) == 0) {
//判断是否为头部
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
//获取为1的放在hi中
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//lo链表的处理
if (loHead != null) {
//如果小于7,那么当做链表处理
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
//转换成树
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
//同上
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
//把树转换成链表
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
转化成红黑树的详情本文就没详细介绍了,我相信很容易看懂. 这里借`美团点评技术团队`的一张图来展示一下put方法的流程
get方法
逻辑其实很简单,就是首先通过hashcode确认位置,然后分数据结构类型继续不同方式的查找
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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 &&((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;
}
remove方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 1.寻找是否存在,如果存在分数据类型分别处理
* 2.如果为树,稍微复杂一点,需要判断是否要转换成链表,然后就是树的移动
*/
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 &&(p = tab[index = (n - 1) & hash]) != null) {
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);
}
}
//找到了对应的节点
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
//指向node的下一个
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
本文就不看树的详细删除过程了,内容太多太长.可以自己想象一下树的删除过程. replace方法
@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;
//与linkedHashMap有关
afterNodeAccess(e);
return oldValue;
}
return null;
}
从基本的方法中我们可以看出,最复杂的就是put方法,put方法设计非常多的方法,后续的get,replace,remove都是建立在put方法基础之上.
补充方法
上面介绍了几个基本的方法,另外现在介绍一些有用的小方法.都是在Java8新增的.
merge方法 merge方法的主要作用是如果不存在就进行添加,如果存在的话按照自己指定的remappingFunction
进行操作,如果操作之后value为null的话删除元素,否则替换,下面是代码详情
public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
//扩容操作
if (size > threshold || (tab = table) == null ||(n = tab.length) == 0)
n = (tab = resize()).length;
//通过key找到元素,分为树和链表两种情况
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
//存在旧的结点,进行合并操作
if (old != null) {
V v;
if (old.value != null)
//具体的合并操作
v = remappingFunction.apply(old.value, value);
else
v = value;
//合并成功,执行afterNodeAccess方法,子类linkedHashMap有用
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
//合并之后没有值,删除元素
else
removeNode(hash, key, null, false, true);
return v;
}
//没有旧结点,直接添加,考虑扩容,链表和树的情况
if (value != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}
compute方法
public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
//省略部分基本和merge方法的前半段一直,不重复展示
//获取旧元素
V oldValue = (old == null) ? null : old.value;
//计算出新值
V v = remappingFunction.apply(key, oldValue);
//存在旧元素,旧赋值到旧元素上
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
//计算结果为空则删除
else
removeNode(hash, key, null, false, true);
}
//r如果没有旧元素,但计算出的值不为空,添加操作,和merge方法相同,省略
return v;
}
看到这里我相信大家都有疑问,为什么要放两个几乎相同的两个方法,
我们详细对比一下两个方法,发现会有几点不同:
- 参数不同,merge方法要求value不为null,compute方法没有要求;
- merge方法要求old.value也不为null.compute的方法依然没有要求.
另外compute还有两个扩展方法,简单介绍一下.
computeIfAbsent方法 如果oldvalue不为null就不替换,如果计算后的值不存在直接返回,否则如果old不为null,通过key计算出值替换,否则添加到map中
public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {
//扩容等操作省略,同上
V oldValue;
if (old != null && (oldValue = old.value) != null) {
afterNodeAccess(old);
return oldValue;
}
//注意这里
V v = mappingFunction.apply(key);
if (v == null) {
return null;
} else if (old != null) {
old.value = v;
afterNodeAccess(old);
return v;
}
//添加操作省略
}
computeIfPresent方法 不进行扩容的判断(因为不存在找不到就添加这样的操作).直接通过key查找,如果找到,且计算的结果不为null,替换,否则就直接删除
public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
//注意这里
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
else
removeNode(hash, key, null, false, true);
}
return null;
}
再次总结一下三个compute方法的异同点
- compute方法和computeIfPresent方法都需要oldValue,computeIfAbsent不需要
- compute的remappingFunction.apply方法不限制oldvalue是否为null,但是computeIfPresent限制必须不为null才进行,computeIfAbsent方法必须要old或者oldvalue为null才会进行后续操作
- computeIfPresent只有oldvalue存在则进行apply方法,然后根据条件替换或者删除操作,而compute和computeIfAbsent方法则是如果old不存在,还会根据条件额外进行添加操作
简单点说就是:
- 如果是不存在oldvalue才进行操作,这也可以认为我们在声明了
String a = null
这样的操作,现在需要进行初始化,选择computeIfAbsent方法, - 如果必须存在oldvalue才操作,而且只进行删除或者修改的操作则选择computeIfPresent方法,类似看看还有没有修改的价值,没价值就干掉了.
- 其他选择compute方法
快速失败和安全失败问题
一:快速失败(fail—fast)
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。
二:安全失败(fail—safe)
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
解答
解释一下刚才在文章设置的悬念,为什么要把newNode方法单独提出来,其实这里很大一部分原因是因为LinkedhashMap会需要重写此方法进行额外的操作.具体是什么可以自己查看一下源码,或者看我的另一篇文章(还没发布),map集合类总结.
总结
改版后的HashMap看起来更加的庞大和神秘了,因为相比之前看起来可能方法更大而且还有红黑树这个数据结构,看到代码就会让人感觉好难受,同时也呼吁每个人都不要写这么庞大的方法.尽可能将方法拆小.变得更加的简洁明了.但是Java8对HashMap的改变,使得HashMap在一定程度上提升了性能,并且还新填了不少的方法.
当然本文只是个人的一些看法,如果存在不足或者错误的地方,欢迎大家指正!!!
结语
与君共勉!!!