阅读时间:2019.1.11–1.17
java版本:1.8
一、概述
在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
在JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值8时,将链表转换为红黑树,这样大大减少了查找时间
二、关键属性
transient Node<K,V>[] table;//hash桶数组
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;//修改次数
int threshold;//临界值 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
final float loadFactor;//加载因子
三、节点Node
1.节点Node
HashTable中的节点Node<K, V>,每个hash桶都是这些Node的一个单向链表。是一个Map.Entry<K,V>的实现,所以可以用for(Map.Entry<K,V> e:map)来遍历所有的Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> 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);
}
//设置新的value 同时返回旧value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true
public final boolean equals(Object o) {
if (o == this)//自己和自己比较,一定是ture
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;//key等,value也相等,返回true
}
return false;
}
}
2.hash算法
(1)首先获取对象的hashCode()值,然后将hashCode值右移16位,然后将右移后的值与原来的hashCode做异或运算,返回结果。(其中h>>>16,在JDK1.8中,优化了高位运算的算法,使用了零扩展,无论正数还是负数,都在高位插入0)。key.hashCode()是调用C的算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.resize()扩容算法
1).在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
2).每次扩展的时候,都是扩展2倍;
3).扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
方法分为两个步骤:
第一步:确定新表的容积newCap,新表的临界值newThr
1.如果oldCap旧容量>0,来确定新table的临界值;如果oldCap*2<最大值&&oldCap>默认值16扩容一倍。
2.如果oldThr旧临界值>0,(即此时是用map来初始化新表,不进行扩容)
3.其他情况:newCap新容量=16,新临界值=0.75(加载系数)*16(默认容量)
第二步:扩容:遍历所有节点
1.节点为叶子结点e.next == null,直接把e放入对应的hash桶中
2.节点是红黑树节点,(e instanceof TreeNode),通过红黑树的方法拆分并挂到新的table桶上
3.其他节点:依据(e.hash & oldCap) 是否等于0,把原hash桶内的链表拆分成两个链表loHead和hiHead
loHead放在原hash桶里,hiHead放到扩容后的对称的hash桶里
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
---//第一步
if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
threshold = Integer.MAX_VALUE;
return oldTab;//不再进行扩容,返回
}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY){
//如果当前hash桶数组的长度在2倍扩容后小于最大容量 && oldCap>默认值16
newThr = oldThr << 1; // double threshold 双倍扩容阀值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);
}
---//用map初始化时(上面的else if),需要计算出表的对应临界值
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];//新建hash桶数组
table = newTab;//将新数组的值复制给旧的hash桶数组
---//第二步
if (oldTab != null) {//进行扩容操作,复制Node对象值到新的hash桶数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {//如果旧的hash桶数组在j结点处不为空,复制给e
oldTab[j] = null;//将旧的hash桶数组在j结点处设置为空,方便gc
if (e.next == null)//如果e后面没有Node结点
newTab[e.hash & (newCap - 1)] = e;//直接对e的hash值对新的数组长度求模获得存储位置
else if (e instanceof TreeNode)//如果e是红黑树的类型,那么添加到红黑树中
((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 {//做一个循环,把桶内数据根据(e.hash & oldCap) == 0分到两个链表里,
//一个以loHead为头节点,一个以hiHead为头节点
next = e.next;//将Node结点的next赋值给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);
//把loHead链表插到原hash桶里,把hiHead链表插到扩容后对称的桶里
if (loTail != null) {//如果loTail不为空
loTail.next = null;//将loTail.next设置为空
newTab[j] = loHead;//将loHead赋值给新的hash桶数组[j]处
}
if (hiTail != null) {//如果hiTail不为空
hiTail.next = null;//将hiTail.next赋值为空
newTab[j + oldCap] = hiHead;//将hiHead赋值给新的hash桶数组[j+旧hash桶数组长度]
}
}
}
}
}
return newTab;
}
四、初始化
初始化构造方法有四种:
HashMap(int initialCapacity, float loadFactor) 自定义容量,和加载系数loadFactor
HashMap(int initialCapacity)自定义容量,用加载系数loadFactor=0.75f
HashMap()默认初始容量16,加载系数0.75
HashMap(Map<? extends K, ? extends V> m) 默认初始容量16,加载系数0.75,并用于个map初始化自己
//自定义的
public HashMap(int initialCapacity, float loadFactor) {//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);//获取该容量下的临界值
}
//返回cap最大的2的n次方,例如cap=100时,返回128;当cap=1000时,返回1024;
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;
}
用map来初始化时。因为传入的oldMap的临界值等会与要创建的newMap不一样,所以在不能直接复制,
而是遍历oldMap,按照新的临界值插入新的newMap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size,hash表为空,计算容量及加载系数
float ft = ((float)s / loadFactor) + 1.0F;//MAXIMUM_CAPACITY=1<<30
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}else if (s > threshold)//如果传入map的size>目前的临界点,重新
resize();//重新计算临界点等,基本逻辑:
//(1)如果目前oldTab==null,计算一下临界值和加载系数,给newTab就好了;
//(2)如果目前oldTab!=null(覆盖旧表),要根据新的临界值,遍历所有newTab节点,把
// 把旧表删掉,按新的临界值挂节点
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方法
//功能性
public boolean containsKey(Object key)
public boolean containsValue(Object value)
//增
public V put(K key, V value)
public void putAll(Map<? extends K, ? extends V> m)
public V putIfAbsent(K key, V value)
//删
public V remove(Object key)
public void clear()
public boolean remove(Object key, Object value)
//改
public boolean replace(K key, V oldValue, V newValue)
public V replace(K key, V value)
//查
public V get(Object key)
public V getOrDefault(Object key, V defaultValue)
//函数式
public V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction)
public V computeIfPresent(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)
public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction)
public V merge(K key, V value,BiFunction<? super V, ? super V, ? extends V> remappingFunction)
public void forEach(BiConsumer<? super K, ? super V> action)
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function)
1.功能性方法
containsKey(Object key)是否存在key,getNode方法在其他方法中也多次使用
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//(表不空&&长度>0) && (key的hash所得到的目标hash桶不为空)
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 {//不是红黑树,是链队列,就遍历找是否有对应的key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
containsValue(Object value)是否存在值,两层循环,外层循环hash表,内层循环遍历每个hash桶
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value || (value != null && value.equals(v)))
return true;
}
}
}
return false;
}
2.“增”类方法
V put(K key, V value),最常用的方法。putVal四个地方会用到:put,putIfAbsent,putMapEntries,readObject
putVal分两步:
第一步:插值
如果通过hashMap刚建立,hashtable还是空的,就新建一个hashtable;如果有则下一步插值
插值要区分情况:
1.如果key所对应的hash桶还是空的,就创建新节点,并把它作为该桶的第一个节点
2.否则,按下面的各种情况分别插值:
(1)桶的第一个节点就是应该插入的地方,就把桶第一个节点替换掉
(2)如果桶里是红黑树(桶内已有超过8个节点),按红黑树插入putTreeVal
(3)如果桶里是链表(桶内已有1~7个节点):
如果有相同key的节点,用新节点替换掉;如果没有,就加上新节点;还要判断桶内节点数是否到8,如果到8了,转化成红黑数treeifyBin
第二步:扩容(如果需要)
如果是替换掉了旧节点,不需要扩容,返回旧节点即可;
如果是新加节点,如果hashMap的size超过了临界值,则进行翻倍扩容resize()
treeifyBin非常精妙,他不会对所有超过8的hash桶进行转换,而是会根据哈希表的容量采用两种方式:
第一种:当容量<64(4*16)时,只扩容。默认初始化容量为16,所以当第一次和第二次触发红黑树转化时,会先进行2倍容量扩容:即16->32,32->64。扩容时,会把桶内的链表拆成两个,这样就不超长了
第二种:当容量已经>64时,才会真正的转化成红黑树
public V put(K key, V value) {
//参数4:如果是true则仅仅做查询不插入
//参数5:如果是false,指该方法是在创建hash时被调用
return putVal(hash(key), key, value, false, true);
}
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)//hashtable还没建起来,就建起来
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//如果新节点的key对应的桶为空,就新建一个桶和节点
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;
}
//表转红黑树方法,在有添加entry的方法中均会调用
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();//table为空,或者table长度<64(4*16)为什么?
//因为刚创建table时,是16,但如果一个桶内有至少8个元素了,就要进行扩容
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);//真正的转化成一颗红黑树,返回根节点,在红黑树模块再解读
}
}
void putAll(Map<? extends K, ? extends V> m)
把参数m里所有元素复制进当前的hashmap,如果新map与旧map有重复的key,就用新map替换。
例如原:mapA={(1:a),(2:b)},新的mapB={(1:k),(3:c)},最终结果:{(1:k),(2:b),(3:c)}
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);//见初始化时的,用另一个map初始化当前map的构造函数
}
V putIfAbsent(K key, V value) 当且仅当key不存在时,才会把value插入到对应的key中。
put方法是,当key存在时,会用新值覆盖旧值。putIfAbsent核心方法与put相同,只是参数不同
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
3.“删”类方法
boolean返回值的remove既要检查key,也要检查value,都相等时才会移除
V返回值的remove返回key对应的value,同时移除该对象
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
核心方法removeNode
第三个参数:比较key同时比较value是否与传入参数相同
第四个参数:是否比较value
第五个参数:删除元素后,是否移动其他节点(只有当桶内是红黑树时才会生效)
分为两个步骤:
第一步:找到要删除的节点,与putVal类似,找到的节点为note
先找到对应的hash桶,再根据情况
1.如果桶内头节点即是要找的key,则取出该节点node=p
2.如果桶内是个红黑树,用红黑树的方法找对应的树节点
3.如果桶内是链表,遍历去找
第二步:删除对应的节点node
1.如果node是红黑树节点,用红黑树方法移除节点removeTreeNode
2.如果node是hash桶的头节点,要把头节点指向它的next,然后再删除操作
3.其他情况,直接删除node
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) {//先找到对应的hash桶
---//第一步
Node<K,V> node = null, e; K k; V v;
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);
}
}
---//第二步
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;
}
4.“改”类方法
这两个方法类似remove
boolean返回值的remove既要检查key,也要检查value,都相等时才会用新value替换旧value
V返回值的remove返回key对应的旧value,同时把旧value替换成新value
不再复述
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
5.“查”类方法
核心方法getNode,在功能类方法中已经做了剖析
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
6.函数式方法
(1)compute / computeIfAbsent / computeIfPresent
下面三个方法都可以给map新增一个<key,value>的entry,主要区别是:
compute:不管原key是否存在,都直接覆盖旧的或者创建新的(二元函数式)
computeIfAbsent:当且仅当原key不存在时,才会新增;原key存在时,不会覆盖旧值(一元函数式)
computeIfPresent:当原key存在,并且对应value=null时,对该key的覆盖失效(二元函数式)
compute,接收的是BiFunction参数,这是二元函数接口,所需调用时要形如:
mapA.compute(6, (k,v) -> v=“6”);
整体逻辑过程类似putVal,主要区别在,把第一步插值再细拆成了两步,所以一共三步
第一步:先遍历hash表,找到key所对应的位置,旧值赋为oldValue
第二步:执行函数式方法中的方法,即给value赋值,v是新的value
第三步,做覆盖或者新建节点的操作
简化后的伪代码:
public V compute(K key,BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> old = null;
TreeNode<K,V> t = null;
---//第一步,先遍历hash表,找到key所对应的位置,旧节点old
//省略遍历部分代码,类似putVal
---//第二步,执行函数式方法中的方法,即给value赋值,v是新的value
V oldValue = (old == null) ? null : old.value;
V v = remappingFunction.apply(key, oldValue);
---//第三步,做覆盖或者新建节点的操作
if (old != null) {//旧节点不为空,要做覆盖操作
if (v != null) {//新value不为空,做覆盖赋值
old.value = v;
afterNodeAccess(old);
}else//新节点为空,不仅不覆盖,而且要把原节点删去
removeNode(hash, key, null, false, true);
}else if (v != null) {//旧节点为空,且新value不空
if (t != null)//t是桶内的红黑树,(如果有的话)
t.putTreeVal(this, tab, hash, key, v);
else {//不是红黑树,插链表,如果桶内节点超过8个,转化为红黑树
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
computeIfPresent:接收的是BiFunction参数,这是二元函数接口,所需调用时要形如:
mapA.computeIfPresent(6, (k,v) -> v=“6”);
当且仅当原key不存在时,才会新增;原key存在时,不会覆盖旧值。所以比comput方法略简单
分为两个步骤:
第一步:找到key对应的节点,并且原value为null,如果不符合条件,直接返回null
第二步:执行函数式方法,如果新的value是null,删除原节点,如果不为null,覆盖原值
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
else
removeNode(hash, key, null, false, true);
}
return null;
}
computeIfAbsent接收的是Function参数,这是一元函数接口,所需调用时要形如:
mapA.computeIfPresent(6, k->“6”);
与compute代码几乎一样,只在一个地方有区别,即在第三步覆盖原节点时。
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction){
---//第一步,省略代码
---//第二步,省略代码
---//第三步
V v = mappingFunction.apply(key);//取出新的value后
if (v == null) {//如果新的value等于空,直接返回空;compute中是删除节点
return null;
} else if (old != null) {
old.value = v;
afterNodeAccess(old);
return v;
}
/* comput对应代码
if (v != null) {//新value不为空,做覆盖赋值
old.value = v;
afterNodeAccess(old);
}else//新节点为空,不仅不覆盖,而且要把原节点删去
removeNode(hash, key, null, false, true);
*/
---//剩余代码省略
}
(2)merge
功能大部分与compute相同,不同之处在于BiFunction中apply的参数,入参为oldValue、value,调用merge时根据两个value进行逻辑处理并返回value
注意到,merge方法接收四个参数但是它和compute一样,接收的是BiFunction方法。
例如 mapA.merge(4, “4”, (k,v) -> (v==“5”)?“6”:null);(k=4,v=“4”);如果当前map中key=4的值(oldValue)是否等于“5”来执行后续的操作。(本例中,如果key=4对应的value!=“5”,则删除key=4的entry
差别在第二步:(compute的疑问:所以为什么不用Function接口?使用一个参数就可以阿?)
第一步:先遍历hash表,找到key所对应的位置,旧值赋为oldValue
第二步:执行函数式方法中的方法,即给value赋值,v是新的value
第三步,做覆盖或者新建节点的操作
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
---//第一步,省略代码,先遍历hash表,找到key所对应的位置,旧值赋为oldValue
---//第二步,省略代码,
if (old.value != null)
//执行函数式方法中的方法,根据传入的函数式方法,判定旧值和新值(第二个参数V value)的逻辑关系
v = remappingFunction.apply(old.value, value);
/* compute中的对应处理,实际上由于compute只有三个参数,所以为什么不用Function接口?
V v = remappingFunction.apply(key, oldValue);
*/
---//剩余代码省略
(3)forEach
forEach的函数式方法BiConsumer同compute,是一个二元函数,较简单
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
//外层循环遍历hashtable,内层循环遍历hash桶
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
(4)replaceAll
replaceAll同arrayList的replaceAll功能,都是通过一个函数式方法,对所有value处理,后覆盖回去。
二元函数BiFunction。代码与forEach只有一个差距,就是调用完函数方法后,是否把原值覆盖。
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
//省略代码
//与forEach的区别在此,是否把计算后的新value覆盖原值
e.value = function.apply(e.key, e.value);
//省略代码
}
六、内部类
Hashmap的内部类包括三种
1.容器类:虽然是容器类,但不是真实的容器,核心是迭代器,指向的仍然是HashTable中的对象
KeySet,Values,EntrySet
2.迭代器:包括KeyIterator,ValueIterator,EntryIterator,以上三种均实现虚拟类HashIterator
3.并行迭代器:包括KeySpliterator,ValueSpliterator,EntrySpliterator 。以上三种均实现虚拟类HashMapSpliterator
4.红黑树:放到第七章详细讲
entry最复杂,故按entry进行解读
1.EntrySet
public方法,主要是用于管理两个迭代器,没有太难理解的地方,就不解读了
public final int size()
public final void clear()
public final boolean contains(Object o)
public final boolean remove(Object o)
public final Iterator<Map.Entry<K,V>> iterator()//新建一个EntryIterator
public final Spliterator<Map.Entry<K,V>> spliterator()//新建一个EntrySpliterator
public final void forEach(Consumer<? super Map.Entry<K,V>> action)
public boolean equals(Object o)
public int hashCode()
public boolean removeAll(Collection<?> c)//用迭代器移除所有entry
2.HashIterator
常规迭代器,没有什么特殊的
3.EntrySpliterator
并行迭代器,与arrayList类似主要提供三种服务
trySplit:把原HashMap拆成两个Map,分别用一个EntrySpliterator管理
tryAdvance:对单个entry执行处理,如果有剩下元素未处理返回true,否则返回false
forEachRemaining:对剩余的entry进行处理
public EntrySpliterator<K,V> trySplit()
public void forEachRemaining(Consumer<? super Map.Entry<K,V>> action)
public boolean tryAdvance(Consumer<? super Map.Entry<K,V>> action)
七、测试
public static void main(String[] args) {
HashMap<Integer,String> initMap = new HashMap<Integer,String>();
for(int i=0;i<5;i++) {
initMap.put(i, String.valueOf(i));
}
//1.用map初始化
System.out.println("测试组1:测试用map初始化,");
HashMap<Integer,String> mapA = new HashMap<Integer,String>(initMap);
mapA = new HashMap<Integer,String>(initMap);
mapA.forEach((k,v)->System.out.println(" key = " + k + ", value = " + v));
System.out.println(mapA.size());
mapA.put(7, null);//添加一个entry<7,null>作为测试
//2.测试computeIfAbsent,当原key已经存在时,不会覆盖旧的
System.out.println("测试组2:computeIfAbsent,添加一个未存在key的entry:<5,‘5’>,一个已存在key的entry:<3,'13'>");
System.out.println("一个未存在key的entry:<5,‘5’>添加成功,一个已存在key的entry:<3,'13'>添加不成功");
mapA.computeIfAbsent(5, k->"5"); //新的key:新的value --->新建节点 成功
mapA.computeIfAbsent(3, k->"13");//旧的key:新的value --->覆盖节点 失败
mapA.computeIfAbsent(4, k->null);//旧的key:新value=null --->不会删除原节点
mapA.forEach((k,v)->System.out.println(" key = " + k + ", value = " + v));
//3.测试compute,不管原key:value什么样,都直接覆盖旧的或者创建新的
System.out.println("测试组3:compute,添加一个未存在key的entry:<6,‘6’>,一个已存在key的entry:<3,'13'>,一个原value=null的entry");
System.out.println("三个entry均添加成功,不管原key:value什么样,都直接覆盖旧的或者创建新的");
mapA.replace(3, "3");
mapA.replace(7, null);
mapA.compute(6, (k,v) -> v="6"); //新的key:新的value --->新建节点 成功
mapA.compute(3, (k,v) -> v="13"); //旧的key:新的value --->覆盖节点 失败
mapA.compute(7, (k,v) -> v="7"); //旧的key:旧value=null --->覆盖节点 失败
mapA.compute(4, (k,v) -> v=null); //旧的key:新value=null --->删除原来的节点
mapA.forEach((k,v)->System.out.println(" key = " + k + ", value = " + v));
//4.测试computeIfPresent
System.out.println("测试组4:computeIfPresent,添加一个未存在key的entry:<8,‘8’>,一个已存在key的entry:<3,'13'>,一个原value=null的entry");
System.out.println("原value=null时,会失败");
mapA.replace(3, "3");
mapA.replace(7, null);
mapA.put(4, "4");
mapA.computeIfPresent(8, (k,v) -> v="8"); //新的key:新的value --->新建节点 失败
mapA.computeIfPresent(7, (k,v) -> v="7"); //旧的key:原value=null --->覆盖节点 失败
mapA.computeIfPresent(3, (k,v) -> v="13");//旧的key:新的value --->覆盖节点 成功
mapA.computeIfPresent(4, (k,v) -> v=null);//旧的key:新value=null --->删除原来的节点
mapA.forEach((k,v)->System.out.println(" key = " + k + ", value = " + v));
//5.测试merge
System.out.println("测试组5:merge,修改key=4的entry");
mapA.replace(3, "3");
mapA.replace(7, null);
mapA.put(4, "4");
mapA.merge(4, "6", (key,newV) -> ("4"==newV)?"4":newV);
mapA.forEach((k,v)->System.out.println(" key = " + k + ", value = " + v));
//6.测试replaceAll
System.out.println("测试组6:replaceAll,用函数方法处理所有的entry,再覆盖回原map");
mapA.replace(3, "3");
mapA.replace(7, null);
mapA.put(4, "4");
mapA.replaceAll((k,v) -> v+v);
mapA.forEach((k,v)->System.out.println(" key = " + k + ", value = " + v));
//7.测试Spliterator
System.out.println("测试组7:Spliterator,拆分原map");
Spliterator<Map.Entry<Integer,String>> split = mapA.entrySet().spliterator();
Spliterator<Map.Entry<Integer,String>> mapB = split.trySplit();
System.out.println("分割后前一半:");
split.forEachRemaining(e->System.out.println(" key = " + e.getKey() + ", value = " + e.getKey()));
System.out.println("分割后后一半:");
mapB.forEachRemaining(e->System.out.println(" key = " + e.getKey() + ", value = " + e.getKey()));
}
八、总结
1.HashMap线程不安全
2.默认初始化设置:hashTable的值为16,加载系数0.75
3.如果设置hashTable值,则实际hashTable的容量是大于设置值的最小二进制数。
例如,传参19,实际32
4.扩容后,新hashtable是原来的2倍,并且会把原桶内拆分成两个链表hi和lo,
lo放在当前桶里,hi放在扩容后对称的那个桶里(tab[j+oldCap] )
5.当某个桶内元素达到8个时,不是直接转成红黑树,而是先尝试把hashtable扩容到64,从而减少每个桶里的元素;当已经扩容到64后,才会转化成红黑树
红黑树的部分待之后结合TreeMap源码