Java源码学习--HashMap

Java源码学习--HashMap 由于HashSet的实现原理是HashMap,所以我们先从HashMap开始学起。
一、重要属性 HashMap的属性很多,比ArrayList还要多,这里就挑选一些重要的属性来说明一下。
静态属性

  • DEFAULT_INITIAL_CAPACITY:该属性是一个final的int,值为16,代表着默认初始容量
  • MAXIMUM_CAPACITY:该属性是一个final的int,值为2^30,代表最大容量
  • DEFAULT_LOAD_FACTOR:该属性是一个final的float,值为0.75f,在进行扩容的时候会用到
普通属性
  • table:该属性是一个Node[],和ArrayList类似,是HashMap的实际容器
  • size:该属性是一个int,代表当前容器中键值对的个数
  • threshold:该属性是一个int,大部分情况下为capacity*loadFactor
  • loadFactor:该属性是一个float,是用户在创建HashMap的时候指定的加载因子,限制table中可以存放元素所占table.length的比例,默认为0.75
二、Node类 HashMap中的key和value是作为一个Node进行存储的:
static class Node implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = https://www.it610.com/article/value; this.next = next; }public final K getKey(){ return key; } public final V getValue(){ return value; } public final String toString() { return key +"=" + value; }public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); }public final V setValue(V newValue) { V oldValue = https://www.it610.com/article/value; value = newValue; return oldValue; }public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry e = (Map.Entry)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }

值得注意的有一点:Node的equals方法是将key和value的equals方法都返回true才返回true
三、重要方法
HashMap的方法有很多,先从构造器说起:
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); }

可以看到,开始时检查传入的两个变量是否合法,之后便是简单的将两个属性初始化。
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }

这个重载的构造器只是第二个参数设置为了内置的默认值(0.75)
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }

这是我常用的构造器,令人奇怪的是,本来以为会将两个参数都设置为内置的默认值,但是这里并不是回调第一个构造器,而是直接只初始化了loadFactor,其他属性不管了!!
还有一点要提醒的是:构造器干的事情太少了,只是处理了两个属性,根本没管table这个属性,所以到这里table还没有初始化(为null),而table的初始化就发生在第一次往HashMap中添加元素的时候,接下来先看看是如何初始化table的。
其在调用put方法添加元素的时候首先会有一个if语句单独处理table为null的情况:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; ...... }

可见最终是在resize()方法处理的:
final Node[] resize() { Node[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; else {// zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap]; table = newTab; return newTab; }

上面的代码是当使用无参构造器初始化HashMap且第一次调用put方法是resize()方法实际执行的代码(没有执行的代码删去了),此时HashMap的状态为:
  • table:初始化完毕,数组的长度为DEFAULT_INITIAL_CAPACITY=16
  • threshold:初始化为了(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)=0.75*16
  • loadFactor:在构造器中初始化为了DEFAULT_LOAD_FACTOR=0.75
hash方法 都知道HashMap的原理是哈希,就先来看看贯穿全文的hash方法的实现:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

可见,HashMap用来进行坐标映射的值并不是key.hashCode()方法,而是(h = key.hashCode()) ^ (h >>> 16)
1. put方法 put方法的作用是将Map中特定的value和特定的key关联起来,如果key已经存在,则覆盖原来的value
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }

可见put方法的实现是回调putVal方法
putVal 该方法的后两个参数在put方法调用的时候分别被赋予了false和true,该方法的注释中说:如果onlyIfAbsent被设置为了true,那么put就不会覆盖原来的value;如果evict被设置了false那么table属性处于正在建立的状态,我们先简单记住该属性一般情况调用的时候都是为true
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; // 为hash算得位置的节点对象 int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; -------------------重点 1------------------- if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node e; K k; --------------重点 2 ---------------- if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); --------------重点 3------------------ 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; } }----------------重点 4--------------- if (e != null) { // existing mapping for key V oldValue = https://www.it610.com/article/e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } }---------------重点 5---------------- ++modCount; if (++size> threshold) resize(); afterNodeInsertion(evict); return null; }

该方法的实现有些长,需要关注的点有注释:
  • 重点 1:这里的if语句中透露出了HashMap中用作table数组下标的并不是hash方法生成的int,而是(table.length - 1) & hash(方法中的n实际为table.length这里直接替换了),这个等价于hash%(table.length)只不过按位操作更加快(注意:这里是因为n在后面可以看到,n永远都是2的幂,所以这里按位与就等价于取余)
  • 重点 2:这里的if语句的意义是,想要put的位置在table中已经有元素了,而且该位置的链表的头一个元素的key和putVal的参数是一致的(注意其判断条件:该节点的hash和参数一致,而且key的equals方法返回true,二者缺一不可)
  • 重点 3:能够进入该else语句说明,想要put的位置在table中已经有元素了,但是那个链表头部的key和参数中的key并不是同一个;该else分支里的第一个if语句的作用是:沿着table中该位置的元素链表往下找,最终添加到链表的尾部;第二个if语句的作用是如果在沿着链表找的过程中,有一个中间节点的key就是目标key,那么就停止往下找了
  • 重点 4:记得一点重点2、3、4中的代码能够执行的前提是想要put的位置已经有元素了;而重点2、3分别对应该位置链表头元素即为目标元素和相反情况;而经历了重点2、3之后,变量e即为需要put的元素的最终位置,这时候重点4只需执行写入value即可
  • 重点 5:该处的代码和重点4是互斥的,进入到这里说明put方法的结果是往table数组中添加了一个新元素,而不是在其某一个位置的链表尾部,这就涉及到是否需要扩容了
总结:从这里可以看到整个put方法的哈希过程:1. 首先根据hash值计算对应的table数组的下标,通过hash值除以数组长度区域简单的到 2. 然后从该位置开始比对key是否相同,一直到该位置的链表的最后 3. 写入value值
还有一点需要注意的是:HashMap在put的时候对value没有任何约束和检查;而对于key,即使为null也是ok的,hash对为null的key,直接返回0,而在后面的对比key的时候null之间==直接返回true比对成功,所以key为null的永远只会占用一个位置
2. get方法 get方法是另外一个重要的核心方法,其作用是返回和特定的key绑定的value:
public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; }

可见,其具体的实现是在getNode方法
getNode 该方法的两个参数的作用显而易见:
final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {-----------------重点 1------------------- 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)first).getTreeNode(hash, key); do { ---------------------重点 2--------------------- if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }

该方法的实现很简单:从first位置沿着链表一个一个找就是了,没有什么难点,唯一需要注意的就是判断是否找到的依据是该节点的hash和参数一致,而且key的equals方法返回true,二者缺一不可
总结:1. HashMap的put和get方法都支持key为null的情况 2. 通过get和put方法判断是否找到的依据我们可以理解在学习Java的时候强调的在重写hashCode方法以及equals方法时的要点:当equals方法返回true的时候,其hashCode方法返回的值一定要相同,否则作为key的时候value放入HashMap之后就找不到了;但是hashCode返回值相同并不要求equals方法返回为true。(equals返回true但hash不同并不会导致bug和程序崩溃,但是它违反了hash的初衷,hash的初衷就是为每一个key维护一个value,如果hash不同的话可能出现有两个相同的key同时存在)
3. resize 上面说到put方法时,其重点5处的代码就是进行扩容操作,接下来就来看看resize方法是如何实现扩容的:
final Node[] resize() { Node[] 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; }-----------------重点 1------------------- 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); }------------------重点 2-------------------- threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node[] newTab = (Node[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) {-----------------重点 3-------------------- oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; -----------------重点 4------------------ 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); -------------------重点 5-------------------- if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

该方法有些长,我们还是通过几个重点来将其分割成几个小块:
  • 重点 1:此处实现了将newCap设置成oldCap的二倍,而oldCap为table.length;将newThr设置成了oldThr的二倍,而oldThr为HashMap的threshold属性(这里可以看出capacity被维护成了2的幂,所以才能够支持通过按位与来等价取余操作)
  • 重点 2:此处将HashMap的threshold属性设置为了newThr(也就是原来的两倍);同时,将table属性重新赋值为一个length为原来两倍的新数组
  • 重点 3:首先要注意的是这里第一句将原来数组该位置设置为了null,不仅想起ArrayList在扩容的时候也会将老数组的元素都置为空,还是为了能够更早的进行垃圾回收,这是一个优秀的习惯--把废弃的对象置为null
  • 重点 4:这里是一个doWhile循环,是用来处理链表的;其中e代表当前的链点,大多数情况下hiHead代表的是新链表的头部,hiTail代表新链表的尾部;而有些情况下((e.hash & oldCap) == 0),loHead代表的是新链表的头部,loTail代表新链表的尾部
  • 重点 5:与其说是重点,不如说是疑点,从重点4处的对特殊情况的处理((e.hash & oldCap) == 0); 我们发现,新的table中将重点 4中特殊情况的节点都分布在了[0, oldCap]范围内;而其他的分在了剩余的地方;没有看懂这个操作的深意,猜测是将所有链表更加均匀的分布在新的table数组中?
疑问:创建新的table自然希望将所有的链表“削的更短”,那为什么不将原链表上每一个节点通过hash值重新计算其在新的table数组中的位置呢,而是还将其保留在原链表上?
4. threshold和loadFactor属性的作用 上面提到过在使用无参构造器初始化HashMap对象的时候table数组的初始化时在第一次putVal的时候,回调resize()方法进行的。在resize()方法中第一组if-else语句由于oldCap和oldThr都为0,所以进入了最终的else分支
newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

而在putVal方法中每次添加完元素后,最后会有一个判断是否需要resize():
if (++size > threshold) resize();

从threshold被初始化为(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)时,以及判断是否需要resize扩容的条件(++size > threshold),可以看出:
  • loadFactor:该属性的意义是限定何时扩容的上限,当table中元素的个数超过这个比例的时候为了减少“冲突”就需要进行扩容,这个值设定的越大,则在HashMap添加的元素多了的时候,新的元素会有更大的概率添加到某一个链表的尾部,而不是直接添加到table中;这就导致了在调用get方法获取的时候,极大的可能是在链表上遍历,而不是直接通过table的下标索引得到,这就失去了哈希的优势;但是如果这个属性设置的太小了,则会增加扩容的频率,同样也会浪费很多性能。
  • threshold:该值无非就是capacity x loadFactor,在判断是否扩容的时候用到(++size > threshold)
5. remove方法 HashMap的remove相对于put和get来说,用的要少一些,但是也是很重要的一个方法。
public V remove(Object key) { Node e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }

可见其就是回调了removeNode方法
removeNode 该方法的第四个参数含义为当为true时,通过hash和key找到的节点的value和第三个参数相等时才能够移除;第五个参数的含义为当为false的时候,删除节点的时候不会删除其他的节点
final Node removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node[] tab; Node p; // table中通过hash算出的下标的元素 int n, index; // n为table的length;index为hash算出的下标; 之后在沿着链表查找的时候代表当前链点的前一个链点,搭配node变量完成链点的删除 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node node = null, e; // node为最终的节点;e为在链表中查找时当前的链点 K k; V v; -------------------重点 1----------------------- if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; --------------------重点 2--------------------- else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode)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)node).removeTreeNode(this, tab, movable); -----------------重点 3----------------- else if (node == p) tab[index] = node.next; -----------------重点 4---------------------- else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }

  • 重点 1:这里的情况代表,需要删除的节点就是table通过index找到的元素,也即链表的表头,这里唯一的就是node = p这一句
  • 重点 2:这里的情况代表,需要删除的节点位于table某一个位置的链表之中,采用的就是一个个查找,这时候变量p表当前链点的前一个链点,变量e代表当前的链点,node代表目标链点,也就是说最后找到目标链点的时候p.next即为node
  • 重点 3:这里删除节点的操作与重点1的情况相呼应
  • 重点 4:这里删除节点的操作与重点2的请况相呼应
提醒:根据源码中“清空”必定要赋值为null的特点,我们可以看到remove中返回了node节点,这也就是为何没有将删除的节点赋值为null的代码,所以我们在使用完了node之后一定要手动将其赋值为null,以呼应源码
四、非重要方法
1. clear 该方法就是清空HashMap中的键值对,首先想到的就是直接将table重新new一个很方便,但是源码中并不是这么做的,其又是一个一个的赋值为null:
public void clear() { Node[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }

2. keySet 该方法是获取HashMap中所有的key,作为一个Set返回:
public Set keySet() { Set ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; }

可见,由于第一次调用该方法的时候,keySet为null,所以第一次返回的是KeySet对象,而之后返回的一直是该对象了。
KeySet类 该类很简单,值提供了访问HashMap中Key的方法,并没有提供添加和修改的方法:
final class KeySet extends AbstractSet { public final int size(){ return size; } public final void clear(){ HashMap.this.clear(); } public final Iterator iterator(){ return new KeyIterator(); } public final boolean contains(Object o) { return containsKey(o); } public final boolean remove(Object key) { return removeNode(hash(key), key, null, false, true) != null; } public final Spliterator spliterator() { return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer action) { Node[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node e = tab[i]; e != null; e = e.next) action.accept(e.key); } if (modCount != mc) throw new ConcurrentModificationException(); } } }

KeySet的clear是调用HashMap的clear方法,会清除所有的键值对;而remove方法也是调用的HashMap的同名方法。值得一提的是该类的forEach方法:同样因为其保存了HashMap当前状态的modCount属性,因此通过forEach方法获取到每一个key之后,不能够通过该key来调用remove、put等方法
3. values 该方法的思想和实现和keySet方法一致:
public Collection values() { Collection vs = values; if (vs == null) { vs = new Values(); values = vs; } return vs; }

Values类 该类的思想和实现和KeySet如出一辙,需要注意的点也是一样的:
final class Values extends AbstractCollection { public final int size(){ return size; } public final void clear(){ HashMap.this.clear(); } public final Iterator iterator(){ return new ValueIterator(); } public final boolean contains(Object o) { return containsValue(o); } public final Spliterator spliterator() { return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer action) { Node[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node e = tab[i]; e != null; e = e.next) action.accept(e.value); } if (modCount != mc) throw new ConcurrentModificationException(); } } }

小结:HashMap的values和keySet方法返回的并不是想当然的将其所有的key放入一个Set中或者将所有的value放入一个Collection中;而是各自内部实现的类,而且我们并不能够访问指定的Key或者Value,只能够通过forEach方法来访问目标的值(由于Key和Value都是引用类型对象,所以这里访问的都是复制的值,不管对该值怎么重新赋值,都不会影响到HashMap中的值;还有一点需要提示的是forEach方法的实现是一个一个遍历)
4. HashIterator类 上面的KeySet和ValuesSet类中都有iterator()方法,而返回的是不同的Iterator对象,而这两个方法返回的Itearator对象都继承自HashIterator类:
abstract class HashIterator { Node next; // next entry to return Node current; // current entry int expectedModCount; // for fast-fail int index; // current slotHashIterator() { expectedModCount = modCount; Node[] 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 nextNode() { Node[] t; Node 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; }public final void remove() { Node p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } }

    1. 该方法相当于就是单单增加了一个remove方法,但是需要注意的是,该remove方法虽然最后将expectedModCount = modCount,但是刚开始的时候还是会校验modCount != expectedModCount;所以在使用Iterator不停的通过next()方法访问数据的时候,能够改变HashMap结构的方法只能够调用Iterator的remove方法,其他的一概不可!!!
final class KeyIterator extends HashIterator implements Iterator { public final K next() { return nextNode().key; } }final class ValueIterator extends HashIterator implements Iterator { public final V next() { return nextNode().value; } }final class EntryIterator extends HashIterator implements Iterator> { public final Map.Entry next() { return nextNode(); } }

可见这三个都是增加了一个next方法而已
5. entrySet方法 keySet和values方法是分别获取所有的Key和Set,而该方法则是获取所有的Node节点,其实现和前者是类似的模式:
public Set> entrySet() { Set> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }

EntrySet类
final class EntrySet extends AbstractSet> { public final int size(){ return size; } public final void clear(){ HashMap.this.clear(); } public final Iterator> iterator() { return new EntryIterator(); } public final boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry) o; Object key = e.getKey(); Node candidate = getNode(hash(key), key); return candidate != null && candidate.equals(e); } public final boolean remove(Object o) { if (o instanceof Map.Entry) { Map.Entry e = (Map.Entry) o; Object key = e.getKey(); Object value = https://www.it610.com/article/e.getValue(); return removeNode(hash(key), key, value, true, true) != null; } return false; } public final Spliterator> spliterator() { return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer> action) { Node[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node e = tab[i]; e != null; e = e.next) action.accept(e); } if (modCount != mc) throw new ConcurrentModificationException(); } } }

可以见得,该方法的forEach访问的是所有的节点,也是采用挨个遍历的方式,同样的特点,由于forEach虽然访问的是每一个节点,节点作为参数传递给action.accept的时候,由于是引用类型对象,这里传递的是一个拷贝,不管怎么改变该值,都不会影响到HashMap中的数据。
6. forEach方法 HashMap自己的forEach和其keySet或者valuesSet中需要注意的点一样:
@Override public void forEach(BiConsumer action) { Node[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node e = tab[i]; e != null; e = e.next) action.accept(e.key, e.value); } if (modCount != mc) throw new ConcurrentModificationException(); } }

五、总结
HashMap常用方法知识点:
  1. HashMap中所有通过哈希值查找节点的时候判断的依据都是(hash == nodeKey.hash && nodeKey.equals(key)),二者缺一不可。据此可以理解Java中对Class中hashCode()方法和equals()方法实现的相互制约的关系。
  2. 调用put方法的时候,并不是每一次put之后都会判断是否需要扩容;只有当put的节点直接添加到了table中(也就是成为了链表头部)的时候才会判断是否扩容。
  3. HashMap的get和put方法都支持key为null的情况,null的hashCode返回值做0处理
  4. 其keySet和valuesSet方法并没有真正的一个Set对象,只是返回了两个内部类对象,提供了forEach方法来只读访问HashMap中的Key或者Values数据;当然每个都有自己的Iterator方法提供next()和remove()两个方法来访问数据
  5. 不管是谁的forEach方法,通过forEach方法可以只读访问数据,不能利用得到的数据更改HashMap的结构(put、set、remove等改变modCount的方法)
【Java源码学习--HashMap】其他知识:
  1. loadFactor代表了table数组可以被占用的比例,超过这个比例就要扩容;capacity属性代表table数组的length,size属性代表的才是HashMap中节点的个数;而threshold一般为capacity * loadFactor,只在判断是否需要扩容的时候使用(++size > threshold); 进行扩容后新的capacity和threshold都为原来的2倍
  2. HashMap的任何“删除”的方法(clear和remove系列),都会将每一个节点赋值为null来更好的垃圾回收
  3. HashMap是线程不安全的,除了同步问题之外,通过forEach或者iterator等访问数据也可能出现异常

    推荐阅读