39-45行 注释:主要介绍了HashMap和HashTable的区别,即HashMap允许null作为键、值,而且HashMap不是线程安全的。并且HashMap中的元素不是有序的,特别的,也不保证随着时间推移,这个map中存储的顺序不发生改变。
解析:hashmap是线程不安全的,键、值都允许null的存在,map中的元素不保证有序,随着时间推移可能还会发生变化。
47-54行 注释:HashMap的get、put方法在能够正确将元素散列在桶中的情况下拥有常数级别的表现(即碰撞次数不过多)。而迭代器在Map中的表现情况依赖于初始化的容量与桶的数量+键值对的值的比例,即装载因子。因此,如果希望迭代器的表现良好,则不能够设置过大的初始容量。
我们知道HashMap的方法中,默认的装载因子大小是0.75,初始容量大小是16(旁边还有一句注释,必须是2的次方)
解析:默认装载因子是0.75,默认初始容量是16
56-65行 注释:就像上面提到的,影响HashMap性能的主要有两个因素,一个是初始容量的大小,一个是装载因子的大小。当HashMap中键值对的数量超过当前容量大小*装载因子的时候,整个表会重新进行hash一次,并进行扩容(接近于原容量的两倍)。
解析:每次resize的大小是将原容量扩充接近1倍。
67-76行 注释: 介绍了采用0.75作为默认装载因子的意义。太高的装载因子会减少多余的空间,但是对查找性能很不友好。因此,最好是在初始化HashMap的时候根据它的键值对量对初始容量和装载因子进行相应的调整,确保让整个hashmap重新hash的次数最小化(即提高性能)。如果初始容量*装载因子>键值对的数量,那么重新散列的情况就不会发生。
解析:定
78-85行 注释:如果Hashmap中的大多数键值对都已经有序了,那么给与它一个充分大的容量会比小的好(肯定啊。。。小了又要rehash)。如果说map中存放了大量的重复的键,会影响map的效率。为了改善影响,那么会在键之间采用比较来改善这种情况。
解析:hashmap的散列方法采用的是拉链法,如果有大量重复主键的话,必然会导致碰撞的大量发生。
87-94行 注释:介绍了hashmap的一个特点:线程不安全。如果有多个线程对Map进行增加/删除元素的话,需要在外部对map对象加锁。这个通常是由一些对象封装了相应的映射关系来完成的。
解析:hashmap是线程不安全的,如果有并发操作需要在外部加上互斥锁。
96-100行 注释:接着上面的继续讲,如果不存在这种封装了的对象,那么hashmap需要在初始化之初使用一个包装类:Collections.synchronizedMap
操作像这样:Map m = Collections.synchronizedMap(new HashMap(...));
解析:一种使线程不安全的hashmap变得线程安全的操作。
102-109行 注释:这里介绍了一下集合类迭代器的一个共同特性:快速失败。即当迭代器创建之后,所属的集合有任何结构上的变化(比如增加/删除元素,那种改变已有键值对的不算),都会抛出一个名为:ConcurrentModificationException的异常。除非是使用迭代器本身安全的remove()方法。
解析:迭代器本身有两个元素,一个ModCount,一个ExceptModCount,每次移动迭代器指针都会检查这两个的值是否相等,如果不等就会抛出快速失败的异常,而迭代器本身的人remove()方法会同步修改这两个值,则不会抛出异常。这个是所有集合类迭代器的共同特征。
110-117行 注释:迭代器的快速失败特征是不可靠的,应该说任何依赖于不同步情况下的并发修改都是难以保证正确性的。因此,迭代器的快速失败特征应该只被用于检查bug出在哪里,而不是保证程序的正确性。
145-154行 注释:hashmap虽然在很多实现的性能表现地像哈希表元素存储在桶里,但在map中桶的数量过多时,桶节点会转化成二叉树节点(红黑树)我们知道红黑树是平衡树的一种,它在数据很大时的查找性能很好。当然我们知道,方法大多数还是为一般情况下准备的,因此这种检验桶是否变成了树节点会一定程度上影响性能。
解析:HashMap在桶(一个单链表,因为采用的是拉链法)的键簇(即链表中的元素数目)较大时(这个值是8),桶节点会变成树节点(红黑树),用于提高它的查找性能。但同时这种检验也难免会一定程度上影响平时的性能。(最好的查找性能是一个桶里只有一个元素,因为桶里存放的是发生碰撞的)
156-172行 注释:当桶里的节点全部变成树节点的时候,它们会主要通过比较hashcode变得有序。如果两个键之间都是实现了Comparable接口的(比如常用的原始数据类型+包装类+String都是实现了的),那么它们会通过compareTo()方法进行比较。虽然说这样对于本来hashcode就唯一/已经有序的键值对会比较浪费时间,但是对于hashcode()方法错误的分配以及很多键共享同一个hashcode的情况,这么做是值得的。
解析:对于树节点中的数据,hashmap主要采用比较Hashcode的方法,如果键的类型实现了Comparable接口的方法,那么就会采用compareTo()使它有序。我们知道,hashcode相等,不一定equals(),equals()不一定==。
174-186行 注释:因为树节点占用的空间比较大,近似普通节点的2倍,所以只有当空间足够的情况下才会进行转变(>=8),在小于6的时候又会转变回去。如果hashcode方法分配良好,在理想情况下,桶中转变为树节点的概率应当服从泊松分布。
解析:转化为树节点的阈值是8,退化的阈值是6.树节点占用空间较大。
199-202行 注释:根节点有时候可能不在树中(比如迭代器的remove方法),不过可以通过TreeNode.root()方法恢复。
204-209行 注释:所有适用的内部方法接收哈希码作为一个参数(通常来自公有方法),来避免对键哈希码的重新计算。许多内部方法也接收一个标签参数,通常是当前的表,在resize的时候也能是新的或旧的表。
解析:接收哈希码作为参数,避免重复计算。提高性能。
211-218行 注释:当哈希桶成树型/非树型以及分隔时,我们会保持它们处在同一种遍历顺序当中,并且一定程度上(较小)减轻迭代器操作的负担。在使用比较和插入的时候,为了保持总体的跨平衡有序,我们把键的类型和独有的hashcode作为连接桥梁。
220-226行 注释:桶是朴素桶还是红黑树桶的使用和转换,由于子类LinkedHashMap的存在而变得更加的复杂。hashmap中的一些由添加/删除触发得到钩子方法允许LinkedHashMap在其他情况下保留独立的内部特性。(笔者找到的钩子方法中的一个是void reinitialize()方法)它们也需要map对象实例通过一些实例方法创建新的节点
(
228-229行 注释:并发编程使用类似于SSA的风格,可以减少错误。(SSA:笔者查了一下,这是一种应用在JVM中的对代码的编译方法)
以上是hashmap的一些特性,接下来是源码的逐行分析阅读
- hashCode()方法:将key的哈希码与value的哈希码做异或运算得到。
- setValue(V newValue):返回修改前的(旧的)Value值
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
解析:即普通的获取map中的键值对映射的值,不过注意,因为hashmap允许键、值为null,所以get方法返回Null并不能作为不存在相对应键的判定,而是应该采用containsKey()方法来达到这个效果。
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;
}
解析:该方法是HashMap的核心方法之一。
first是对应的key值对应的hash值,在table数组中对应hash值位置链表的第一个元素。如果要找的key就是这个first,则将该first键值对返回。
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
解析:本方法采用566行实例方法,这也是为什么containsKey能判断键是否存在。因为getNode不会返回Null,除非查找的元素不存在。
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;
}
解析:这是HashMap中最重要的方法之一。接收四个参数,其中后两个布尔型,一个代表在所对应键的值已经存在的情况下,是否执行覆盖;一个代表hashMap的table字段是否处于创建状态(前面提到,table仅在初始化时执行一次(不是终态是因为包括resize操作))。而常用的Put()方法,调用时是
采取覆盖操作,不创建table.
676行 终态方法: final Node<K,V>[] resize()
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;
}
//挪到新位置,原索引+oldCap(n)
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;
}
解析:这是HashMap中核心方法之一,用于拓展map中键值对数组的大小(拉链法的数组)。如果还没有被初始化,那么table数组会被初始化为默认大小(16),否则会拓展为原大小的
两倍。链表中的元素,因为是产生“碰撞”而进去的,所以会有相同的table索引值(table[(table.length-1)&hash]);或者会分散在
偏移量为2的乘方的新的table数组中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
呐,可以看到,如果链表里面只有一个节点(即没有发生碰撞),那肯定是朴素桶了,直接重新计算索引值搬家即可;否则,发生了碰撞,判断当前节点是不是树节点,如果是,执行树节点的分散方法。(注:本方法只会被resize方法调用)
- 1.8中没有rehash获得新的hash值再将newHash&(newCap-1),而是在原基础上利用原hash/容量,将链表中元素分成了要迁移和不迁移的两部分
- 1.8中resize方法保全了链表中原顺序的有序性,1.7则将原链表顺序倒置
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
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);
}
}