最近准备开始系统的看一下jdk实现的源码和了解一些设计模式,先从最经典的容器类入手,看了以后不得不说数学基础是真的重要,若是再给我一次机会我必定更认真地学数学。本篇文章会分析HashMap。首先,在本篇文章开始以前咱们要知道什么是哈希表,链表,红黑树,能够参考我之前的博文进行了解。java
首先HashMap是经过哈希表实现的,哈希函数为取余,哈希冲突解决方法为链地址法,也就是说HashMap的底层是一个数组+链表(红黑树)的结构(数组中的每一个元素可能造成一个链表,链表长度大于8时链表化为红黑树),如图就是一个hashmapnode
下面来具体看一下hashmap时如何实现的。算法
首先看hashmap的属性设计模式
//哈希表数组
transient Node<K,V>[] table;
//键值对的个数
transient int size;
//哈希表数组默认初始大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
//哈希表数组的最大元素个数
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子,扩容时有用,也能够本身构造时指定,默认0.75f
final float loadFactor;
//当前HashMap所能容纳键值对数量的最大值,超过这个值,则需扩容,threshold = capacity * loadFactor
int threshold;
//遍历的entrySet
transient Set<Map.Entry<K,V>> entrySet;
//用于判断是否在遍历的时候进行改变,如果,抛出ConcurrentModificationException
transient int modCount;
//链表的节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
//Entry
final class EntrySet extends AbstractSet<Map.Entry<K,V>>{...}
复制代码
hashmap提供了四种构造函数数组
//无参构造函数,全部的属性都为默认属性
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//传入初始容量,负载因子为默认0.75
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);
}
//经过map构造一个新map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
复制代码
用户传入初始化大小时是如何计算threshold呢?安全
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为31,返回32;cap为7,返回8;cap为9,返回16bash
查找的逻辑为先计算到数组的下标,而后对链表或红黑树进行遍历查找多线程
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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 && // 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;
}
复制代码
first = tab[(n - 1) & hash]
复制代码
即直接经过(n-1)&hash就找到table中元素的下标,n=table.length,hash为算出来的key的哈希值。而table的大小始终是2的n次方,(n - 1) & hash 等价于对 length 取余。能够尝试当n=16时,hash=55,这时对hash%n操做获得的结果为7,(n-1)&hash结果也是7。而至于为何不采用取余操做而使用这种方式,众所周知位运算的速度比取余操做速度快。app
这个问题的意思就是为何hashmap中要本身实现hash(K key)函数。函数
遍历hashmap有大体三种方法
//1.采用foreach迭代entries
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue())
}
//2.使用foreach迭代keys和values
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
//迭代key
for (Integer key : map.keySet()) {
System.out.println("Key = " + key);
}
//迭代value
for (Integer value : map.values()) {
System.out.println("Value = " + value);
}
//3.使用Iterator迭代
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<Integer, Integer> entry = entries.next();
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
复制代码
而foreach咱们知道它只是一种语法糖,事实上在进行反编译后发现foreach实际上就是等价于使用iterator进行迭代的。因此下面直接看迭代器时如何迭代的。
下面来看一下有关迭代器遍历的代码:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
}
/**
* 迭代器
*/
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
// 寻找第一个包含链表节点引用的桶
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
// 寻找下一个包含链表节点引用的桶
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
//省略部分代码
}
复制代码
遍历全部的键值对时,首先要获取键集合EntrySet对象(对key和value进行遍历类似),而后再经过 EntrySet 的迭代器EntryIterator进行遍历。EntryIterator 类继承自HashIterator类,核心逻辑也封装在 HashIterator 类中。HashIterator 的逻辑并不复杂,在初始化时,HashIterator 先从桶数组中找到包含链表节点引用的桶。而后对这个桶指向的链表进行遍历。遍历完成后,再继续寻找下一个包含链表节点引用的桶,找到继续遍历。找不到,则结束遍历。如图:
打印顺序为:54,29,16,43,31,46,60,74,88,77,90
若是链表转为红黑树的话采用的是哪一种遍历方式?
能够看到,在遍历的代码中并无对红黑树单独进行遍历,这时由于这里咱们的红黑树中有一个成员变量指向原链表中的下一个节点,因此,转为红黑树的遍历顺序就是next的顺序。
//新增方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hash函数
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 增长操做的具体实现
*
* @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; //若是是第一次添加会初始化table 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; // 若是键的值以及节点hash等于链表中的第一个键值对节点时,则将e指向该键值对 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //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; } //加入的节点和原来的节点是同一个节点(这里哈希值和equals方法都相同才断定为同一个节点) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //判断要插入的键值对是否存在 HashMap 中 if (e != null) { // existing mapping for key V oldValue = e.value; //onlyIfAbsent 表示是否仅在 oldValue 为 null 的状况下更新键值对的值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //当键值对数量超过扩容阈值时扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 复制代码
总的来讲,大概流程以下:
再增长的时候会进行扩容,扩容的条件为++size > threshold,下面具体来看扩容的实现
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//table已经完成初始化
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
}
//table未完成初始化但阈值已经设置完成,即便用HashMap(int)或HashMap(int,float)构造时会发生这种状况
else if (oldThr > 0) // initial capacity was placed in threshold
//设置阈值为新容量
newCap = oldThr;
//table未初始化,阈值未初始化,即便用HashMap()构造时发生这种状况
else {
//设置初始值
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;
//(e.hash & oldCap) == 0为true判断的是扩容后在原下标,false为要放进新的下标。
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);
//将链表放进新table中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
复制代码
好的,扩容的逻辑就是上文这样,总结一下,扩容主要作了三件事
前文说到当链表长度大于8时要将链表转为红黑树,红黑树长度小于6时要将红黑树转为链表。具体的就是转化分为三个操做:
static final int TREEIFY_THRESHOLD = 8;
/**
* 当table数组容量小于该值时,优先进行扩容,而不是树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
/**
* 将普通节点链表转换成树形节点链表
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//table数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//hd为头节点,tl为尾节点
TreeNode<K,V> hd = null, tl = null;
do {
//将普通节点替换成树形节点
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null); //将普通链表转成由树形节点链表
if ((tab[index] = hd) != null)
//将树形链表转换成红黑树
hd.treeify(tab);
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
复制代码
看了链表变为红黑树的源码,咱们能够得出如下结论:
链表变为红黑树的条件:
链表如何变为红黑树:
由于一开始hashmap设计时并无考虑到对节点进行大小比较,而红黑树是一种有序树,因此要求每一个节点要可比较(实现comparable接口或提供比较器)。因此在树化的过程会经历如下过程:
经过以上方法比较完以后就能进行排序变为红黑树了
拆分就是扩容的时候,红黑树节点要映射到新的哈希表中的对应位置,这个过程叫树的拆分。固然,若是将所有红黑树中的节点从新计算下标插入到新哈希表,由链->树这样也能够,但这样效率很低,因此hashmap的设计多了一个拆分。
// 红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
/*
* 红黑树节点仍然保留了next引用,故仍能够按链表方式遍历红黑树。
* 下面的循环是对红黑树节点进行分组,与上面相似
*/
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
//若是loHead不为空,且链表长度小于等于6,则将红黑树转成链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
/*
* hiHead == null 时,代表扩容后,
* 全部节点仍在原位置,树结构不变,无需从新树化
*/
if (hiHead != null)
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);
}
}
}
复制代码
红黑树的拆分和扩容时对节点的重映射类似
当删除节点时可能形成红黑树的个数小于6个,这时要将红黑树再转化为链表。
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
//遍历TreeNode链表,并用Node替换
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;
}
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
复制代码
由于红黑树中的节点有prev节点,这个节点指向的原链表形态时当前节点的下一个节点,因此在链化的时候就很方便了。
如下为我我的观点
删除大概分为三部
固然,和前面同样,删除链表节点可能会变为删除红黑树节点,有可能删除红黑树节点后会进行红黑树的链表化。下面来看一下实现代码:
//暴露给使用者的删除方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 删除的具体实现
*
* @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) {
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;
//若是键的值与链表第一个节点相等,则将node指向该节点
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,接下来删除node,并修复链表或红黑树
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;
}
复制代码
其中,关于红黑树的维护这里就不具体讲解了,由于我实在以为红黑树的维护思想不难,可是很麻烦,解读的话也差很少和日常红黑树维护的思路相同,就是左旋转,右旋转,变色。红黑树链表化前文也分析过了。
咱们在使用迭代器进行迭代时,当咱们改变hashmap的状态时,会抛出ConcurrentModificationException异常。
抛出这个异常的缘由就是在遍历hashmap的时候,hashmap的数据改变了。咱们在分析迭代器遍历的时候有这么一段代码:
abstract class HashIterator {
//其余代码
final Node<K,V> nextNode() {
//其余代码
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//其余代码
}
}
复制代码
能够看到,这个异常抛出的条件是modeCount!=expectedModCount的时候,而modCount以前说过期hashmap的一个属性,每当进行put,remove操做的时候都会对modCount进行++操做,而exceptedModeCount是一开始遍历时候的modCount。因此说,这个异常抛出的时候表明遍历的时候表发生了改变。这也就是java中的fail-fast机制。这个机制主要是针对多线程环境下的线程安全问题,但事实上hashmap仍是线程不安全的。
咱们想作的无非就是在遍历的时候删除元素
咱们看到哈希表table使用了transient修饰,这个关键词表明这个属性不会被序列化。
咱们知道肯定hashmap使用的哈希函数会用到key的hashCode,而key的哈希code方法可能没有被重写,这样这个方法就会调用Object的hashCode方法,而Object的hashCode方法是本地native方法,那么会形成的问题就是同一个hashmap可能在不一样虚拟机下反序列话时形成每一个键的hash值不同,就会形成键值对对应的位置出错。
可能我须要将hashmap传送到其余计算机上,那么hashmap本身实现了writeObject(java.io.ObjectOutputStream s)和readObject(java.io.ObjectInputStream s)方法。他们做用是该类在序列化时会自动调用这两个方法,没有这两个方法就调用defaultWriteObject()和defaultReadObject()。而hashmap实现了,因此看一下他们是如何进行序列化的,下面只分析写,读就对是相应数据的读就好了。
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (Node<K, V> e : tab) {
for (; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
复制代码
能够看到,hashmap实现序列化的方式就是将hashmap拆解为属性和节点,直接把属性和节点序列化便可
那么hashmap的分析就到此结束,原本是想把有关Map的经常使用实现类都写在一篇的,但真正查看hashmap源码的时候才发现一些实现真的要考虑不少问题和细节,写在一篇里太多太杂。经过此次完全的源码分析我也是感觉到了代码的设计,编写艺术,在一开始查看源码的时候我都以为这些编写人员是否是有一点设计过分,但后来又对每一个细节进行思考,包括查了不少篇网上的博客,我才真的以为设计人员的厉害之处,虽然他们的代码量不少很长,注释也很是多,但每一行代码都有本身的很关键的做用,每一行代码都必不可少,考虑的也很是全面,包括对每个细节的考虑。总的来讲,此次源码的分析不只让我知道了hashmap的实现细节;还教会了我怎样去设计一个完美,通用的代码;还有就是数学的重要性。
下一篇会经过分析ConccurrentHashMap,而后再简单的看其余容器时如何实现的,最后祝我本身期末不挂科。