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