A TIP
看源码的经验并不多,这次刚看起来也是没有头绪,不知从何处看起,但是还是强迫自己硬着头皮看,慢慢的就有了些思路。
稍微的记一下。
1,类的文档注释:英语水平好的看完这个差不多就会有自己的思路去解析这个代码了。
2,构造函数:构造函数一般会重载,可挑参数少的或者常用的,看看其中的成员属性的初始化。
3,成员属性:这个一般会比较多,粗略的捡重要的看下,如构造器中使用的,知道下类型,初始值等。
4,重要的方法:比如HashMap中的put/get方法,这些方法中就会慢慢的扩散开了,像动态扩容,详细的存储方法等。这样整个代码结构就掌握得差不多了,之后你便可以再通览一番。你也可以根据他的工作流程,使用步骤来阅览。
DIFFERENCE
HashMap JDK1.7 和JDK1.8的内容是存在差异的。
jdk1.7使用的是Entry数组作为数据的存储对象。由key的hash值来决定对象存储在数组的那个位置,使用链表结构解决hash值一致的情况,即当两个数据的hashCoe都想同时,他们都会存在entry数组的同一个下标中,而这个数组元素里的key就会形成链表结构。所以当HashMap中的所有元素的hashCode都相同时,他便就相当于一个链表结构。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
jdk1.8则使用是Node内部类数组。同样也是由key来的hashcoe决定其存储位置。但是当key的hash值很多相同时,链表接口会变成红黑树结构。即Node数组当中,每个数组元素的内容超过八个时,这个时候链表结构就会变成红黑树结构。而红黑树的查找元素的时间复杂度O(log n)是低于链表结构的时间复杂度O(n)的。如下图。
transient Node<K,V>[] table;
构造函数
public HashMap() {...}
public HashMap(int initialCapacity) {...}
public HashMap(int initialCapacity, float loadFactor) {...}
public HashMap(Map<? extends K, ? extends V> m) {...}
常用的就是第一个无参的,里面初始化了用来扩容的加载因子loadFactor。其默认值为0.75f。得到的HashMap的初始化大小为16.
this.loadFactor = DEFAULT_LOAD_FACTOR;
第二个构造器可以给定初始大小,他仍会使用默认的加载因子。第三个就可以同时制定初始化大小和加载因子。
第四个构造器是将一个其他map结构的对象转为HashMap。
成员属性
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;
transient Node<K,V>[] table;
这些属性都是可以见名知意的。
-
DEFAULT_INITIAL_CAPACITY ,即HashMap默认的初始化大小。
-
MAXIMUM_CAPACITY ,即HashMap最大容量
-
DEFAULT_LOAD_FACTOR ,扩容用的默认的加载因子
-
TREEIFY_THRESHOLD ,树化阈值。当数组中某个链表结构的长度唱过了该值时,就将链表结构转为红黑树结构。
-
table,就是用来存储数据的Node数组。
重要的方法
我们发现,HashMap在构造器中并没有看到初始化node数组大小的代码。那他在哪呢?
put():
首先我们看put()方法,这个方法很简单,里面调的是putVal()方法,这才是存值得方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal()方法的第一个参数是hash(key),HashMap会根据这个hash值来决定将这个键值对放在Node数组的那个位置。取的时候也会依据这个hash值来获取。
/**
* 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;
}
一段一段来看:
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
每次put之前都会判断node数组即table是否为空,或者其长度为0。如果是的话就调用resize方法。这个方法做的事情就是初始化table或者扩容(详细的我们下面再看),并且返回table。所以,HashMap Node数组的初始化就是在resize()方法中。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
这里主要是同过存储key的hash判断其是否已经存在于数组table中,如果hash不存在那么就将这个键值对存在Node数组table一个新的位置。
因为当key的hash值相同,而数组的长度是一定的,这里先不考率扩容,自然得到的位置p就是相同的。
问题:tab[i = (n - 1) & hash]如何保证不出现数组越界?
这可能是个简单的问题。但是暂时没想到。
下面的else里面则为hash冲突时的处理逻辑。
这个两段代码为处理key相同时则覆盖的逻辑。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
--------------------------------------------
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
如果table的当前位置为树结构则继续存放。
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;
}
}
否则就是遍历链表结构,将下个节点指向该新存放的对象。
但是如果该链表结构的长度超过了树化阈值TREEIFY_THRESHOLD就要调用treeifyBin()将该链表结构转化为树结构。
resize():
这里就先接着讲resize。之前在讲put()根据数组长度和key的hash值来获取对象存放位置时,之所以不考虑扩容的情况,其答案在这个方法里。
首先我们要知道这个方法什么时候会被调用,起码现在我们知道
当map为空的时候需要初始化和当table的size超过阈值而需要扩容的时候。
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;
现在假设我们是table需要扩容的时候进入了该方法。所以他的执行的就是下面这段代码。
而最下面的if判断里的for循环就是当table扩容后,会将原先oldTab里的内容根据新table的长度和hash值获取新的索引存放到newTab中。所以之前put里面也能取到扩容之后的根据长度得到的索引位置的值。
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
.....
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
......
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) {
........
其他的还有许多,就不一一写上来了。