文章目录
Java集合(一)集合框架概述
Java集合(二)ArrayList、LinkedList使用及源码分析
Java集合(三)Vector、Stack使用及源码分析
Java集合(四)HashMap、HashTable、LinkedHashMap使用及源码分析
Java集合(五)HashSet、LinkedHashSet使用及源码分析
Java集合(六)ArrayBlockingQueue、LinkedBlockingQueue使用及源码分析
HashMap
一、HashMap概述
- 1、HashMap的继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
从继承关系看:
- 继承AbstractMap,实现Map接口,可以进行Map(键值对)的相关操作。
- 实现Cloneable接口,可以进行克隆。
- 实现Serializable接口,可以进行序列化。
- 2、HashMap底层结构
简单来说,HashMap的底层结构是数组,但数组中存的不是普通的某种类型的元素,存储的是链表或红黑树(1.7是数组+链表,1.8则是数组+链表+红黑树结构)。
HashMap的结构示例:
- 3、哈希表
HashMap使用哈希表来存储的,哈希表为解决冲突,可以采用开放地址法和链地址法等来解决,Java 中的 HashMap 采用了链地址法。链地址法简单来说就是数组加链表的结合,在每个数组元素上都有一个链表结构,当数据被 hash 后,得到数组下标位置,把数据放在对应数组下标元素的链表上。
怎么理解哈希表呢?可以简单理解为哈希表=哈希函数+数组
。比如一个键值对(key,value),哈希函数可以通过key计算出一个值,这个值就是数组的下标index,然后在数组的index把value存进去,这样就完成了一个键值对的存储。
哈希表有多种不同的实现方式,“拉链法”示例:
如上图:一个长度为 16 的数组中,每个元素存储的是一个链表的头节点,这些元素存储到数组的规则一般是通过hash(key)% len (也就是元素 key 的 hash 值对数组长度取模得到的)
获得的,比如:12 % 16 = 12, 28 % 16 =12,108 % 16 = 12, 140 % 16 = 12,所以 12、28、108 和 140 都存储在数组下标为 12 的链表中。 - 4、为什么HashMap要设计成数组(哈希表)+链表(和红黑树)呢
1. 数组
采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)。
2. 线性链表
对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)。
3. 二叉树
对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)。
4. 哈希表
相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。
哈希表的主干就是数组,存储的是键值对
。数组是HashMap的主体,链表和红黑树是主要为了解决哈希冲突而存在的。
二、HashMap特点
2.1 HashMap特点
- 1、底层实现是 链表+数组+红黑树(JDK 1.8)
JDK 1.8 的 HashMap 的数据结构是数组为主干,链表或红黑树为辅助(当链表节点较少时仍然是以链表存在,当链表节点较多时(大于8)会转为红黑树
)。当一个元素要存储HashMap时,先通过哈希方法找到要存入的数组的下标,然后将value存在对应的位置上。
JDK1.8 中引入了红黑树,大大的提高了 HashMap 的性能。
- 2、HashMap 的底层是个 Node(键值对) 数组
在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。 - 3、增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步
源码中是通过下面3个操作来完成这一步:
1)拿到 key 的 hashCode 值;
2)将 hashCode 的高位参与运算,重新计算 hash 值;
3)将计算出来的 hash 值与 “table.length - 1” 进行 & 运算。
- 4、HashMap 的默认初始容量(capacity)是 16,capacity 必须为 2 的幂次方;默认负载因子(load factor)是 0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。
- 5、HashMap 在触发扩容后,阈值会变为原来的 2 倍,并且会对所有节点进行重 hash 分布
重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。
导致 HashMap 扩容后,同一个索引位置的节点重 hash 最多分布在两个位置的根本原因是:
1)table的长度始终为 2 的 n 次方;
2)索引位置的计算方法为 “(table.length - 1) & hash”。HashMap 扩容是一个比较耗时的操作,定义 HashMap 时尽量给个接近的初始容量值。
- 6、HashMap 有 threshold 属性和 loadFactor 属性,但是没有 capacity 属性。初始化时,如果传了初始化容量值,该值是存在 threshold 变量,并且 Node 数组是在第一次 put 时才会进行初始化,初始化时会将此时的 threshold 值作为新表的 capacity 值,然后用 capacity 和 loadFactor 计算新表的真正 threshold 值。
- 7、当同一个索引位置的节点在增加后达到 9 个时,并且此时数组的长度大于等于 64,则会触发链表节点(Node)转红黑树节点(TreeNode),转成红黑树节点后,其实链表的结构还存在,通过 next 属性维持。链表节点转红黑树节点的具体方法为源码中的 treeifyBin 方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。
- 8、当同一个索引位置的节点在移除后达到 6 个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的 untreeify 方法。
- 9、HashMap 在 JDK 1.8 之后不再有死循环的问题,JDK 1.8 之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。
- 10、HashMap 是非线程安全的,在并发场景下使用线程安全的集合ConcurrentHashMap 来代替。
2.2 HashMap 和 Hashtable 的区别
- 1、继承关系
HashMap是继承自AbstractMap类;
而HashTable是继承自Dictionary类。 - 2、线程安全
HashMap是非线程安全的;
HashTable 是线程安全的。 - 3、效率
因为线程安全的问题(加锁),HashMap要比HashTable效率高。 - 4、对Null key 和Null value的支持
HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
HashMap的key-value支持key-value,null-null,key-null,null-value四种。而Hashtable只支持key-value一种(即key和value都不为null这种形式)。 - 5、初始容量大小和每次扩充容量大小的不同
创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。 - 6、底层数据结构
JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)并且数组中元素数>64时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。 - 7、推荐使用
在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。 - 8、计算hash值的方法不同
Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数来获得最终的位置。
Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。
三、HashMap使用
3.1 常用方法介绍
- 1、构造一个空的 HashMap ,具有默认初始容量(16)和默认负载因子(0.75)
public HashMap()
- 2、构造一个空的 HashMap,具有指定的初始容量和默认负载因子(0.75)
public HashMap(int initialCapacity)
- 3、构造一个空的 HashMap,具有指定的初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor)
- 4、清空HashMap
public void clear()
- 5、如果此HashMap包含指定key,则返回 true
public boolean containsKey(Object key)
- 6、如果此HashMap包含指定value,则返回 true
public boolean containsValue(Object value)
- 7、返回HashMap中Entry构成的Set
public Set<Map.Entry<K,V>> entrySet()
- 8、对此HashMap中的每个元素执行给定的操作,直到所有元素都被处理或操作引发异常
public void forEach(BiConsumer<? super K, ? super V> action)
- 9、返回到指定key所映射的value,不存在该key则返回null
public V get(Object key)
- 10、返回到指定key所映射的值,不存在该key则返回默认值
public V getOrDefault(Object key, V defaultValue)
- 11、判断HashMap中是否包含元素
public boolean isEmpty()
- 12、获取HashMap中key的集合
public Set< K > keySet()
- 13、存入一个键值对,如果key存在则替换原有的键值对
public V put(K key, V value)
- 14、将指定Map的所有映射复制到此HashMap
public void putAll(Map<? extends K, ? extends V> m)
- 15、从该Map中删除指定key的映射(如果存在)
public V remove(Object key)
- 16、删除指定key和指定value构成的键值对
public boolean remove(Object key, Object value)
- 17、替换指定key的value
public V replace(K key, V value)
- 18、只有当指定key和指定value构成键值对时,才替换指定的value
public boolean replace(K key, V oldValue, V newValue)
- 19、返回此HashMap中键值映射的数量
public int size()
- 20、返回此HashMap中包含的value的集合
public Collection< V > values()
- 21、HashMap常见的遍历方式
- 使用For-Each迭代entries;
- 使用For-Each迭代keys和values;
- 使用Iterator迭代;
- 迭代keys并搜索values。
3.2 常见方法使用
//1.构造方法
HashMap<String, String> hashMap = new HashMap<>();
HashMap<String, String> hashMap1 = new HashMap<>(20);
HashMap<String, String> hashMap2 = new HashMap<>(20,0.5f);
hashMap.put("1", "a");
//2.清空HashMap
hashMap.clear();
System.out.println(hashMap); //{}
//3.判断是否包含指定key
hashMap.put("1", "a");
hashMap.put("2", "b");
hashMap.put("3", "c");
System.out.println(hashMap.containsKey("3")); //true
//4.判断是否包含指定value
System.out.println(hashMap.containsValue("dd")); //false
//5.返回HashMap中Entry构成的Set
System.out.println(hashMap.entrySet()); //[1=a, 2=b, 3=c]
//6.对HashMap中的元素执行特定操作
hashMap.forEach((k, v) -> System.out.print("key: " + k + " value:" + v +" "));//key: 1 value:a key: 2 value:b key: 3 value:c
System.out.println();
//7.获取指定key对应的value
System.out.println(hashMap.get("1")); //a
//8.获取指定key对应的value,key不存在就取默认值
System.out.println(hashMap.getOrDefault("2","ee")); //b
System.out.println(hashMap.getOrDefault("5","ee")); //ee
//9.添加元素
hashMap.put("5", "ee");
//10.添加另一个Map的所有元素
hashMap2.putAll(hashMap);
System.out.println(hashMap2); //{1=a, 2=b, 3=c, 5=ee}
//11.删除元素
System.out.println(hashMap.remove("1")); //a
System.out.println(hashMap.remove("2","cc")); //false
System.out.println(hashMap.remove("2","bb")); //false
//12.替换元素
System.out.println(hashMap.replace("1", "dd", "ff")); //false
System.out.println(hashMap.replace("3", "cc", "aa")); //false
System.out.println(hashMap.replace("7", "ii", "kk")); //false
System.out.println(hashMap); //{2=b, 3=c, 5=ee}
hashMap.replaceAll((key,value) -> value+" all");
System.out.println(hashMap); //{2=b all, 3=c all, 5=ee all}
//13.获取value的集合
System.out.println(hashMap.values()); //[b all, c all, ee all]
//14.遍历
for(Entry<String, String> entry : hashMap.entrySet()){
//key = 2, value = b all key = 3, value = c all key = 5, value = ee all
System.out.print("key = " + entry.getKey() + ", value = " + entry.getValue() + " ");
}
System.out.println();
for (String key : hashMap.keySet()) {
//Key = 2 Key = 3 Key = 5
System.out.print("Key = " + key + " ");
}
System.out.println();
for (String value : hashMap.values()) {
//Value = b all Value = c all Value = ee all
System.out.print("Value = " + value + " ");
}
System.out.println();
Iterator<Entry<String, String>> entries = hashMap.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, String> entry = entries.next();
//Key = 2, Value = b all Key = 3, Value = c all Key = 5, Value = ee all
System.out.print("Key = " + entry.getKey() + ", Value = " + entry.getValue() + " ");
}
System.out.println();
for (String key : hashMap.keySet()) {
String value = hashMap.get(key);
//Key = 2, Value = b allKey = 3, Value = c allKey = 5, Value = ee all
System.out.print("Key = " + key + ", Value = " + value);
}
四、HashMap源码分析
先看一些变量:
//默认的初始容量(容量为HashMap中槽的数目)是16,且实际容量必须是2的整数次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量(必须是2的幂且小于2的30次方,传入容量过大将被这个值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
//当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
//桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//存储数据的Entry数组,长度是2的幂
//HashMap采用链表法解决冲突,每一个Entry本质上是一个单向链表
transient Node<K,V>[] table;
//存放具体元素的集
transient Set<Map.Entry<K,V>> entrySet;
//存放元素的个数,不等于数组的长度
transient int size;
//每次扩容和更改map结构的计数器
transient int modCount;
//临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
//负载因子
final float loadFactor;
4.1 Node
在HashMap的底层数组中,存储的元素是Node。Node 是 HashMap 的一个内部类,其实现了 Map.Entry 接口,本质就是一个映射(键值对)。从Node的定义中只有next可以看出,Node是单向的:
//链表节点, 继承自Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //哈希值,用来定位索引位置
final K key; //key
V value; //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; }
//每一个节点的hash值,是将key的hashCode 和 value的hashCode 亦或得到的
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;
}
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;
}
}
从上面源码可以看出:
- HashMap的一个节点由4部分组成:
哈希值、key、value和对下一个节点的引用
。- (通过hashCode()方法)
计算hashcode,是根据key和value的哈希值共同计算的
。- 要比较两个节点是否相等时,key和value都相等才行。
4.2 哈希函数
HashMap中的hash(Object key)方法:
//JDK1.8中的哈希方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap 的数据结构是 链表+数组 的结合,所以希望 HashMap 里的元素位置尽量分布均匀些,使得每个位置上的元素尽量少(最理想的情况是每个位置只有一个元素)。
我们可以将hash
方法的内容分成两部分:
h = key.hashCode(); //1、计算出key的哈希值
h ^ (h >>> 16); //2、高位参与运算
有了key对应的哈希值,就可以使用(tab.length - 1) & hash
来计算该key在数组中的下标,进而进行其他操作。这个步骤在JDK1.7中被封装成方法:
static int indexFor(int h, int length) {
return h & (length-1);
}
不过在JDK1.8中,这个步骤并没有被封装,什么时候用什么时候计算:
n = tab.length;
(n - 1) & hash; //取模运算
综合以上来看,根据key计算对应的数据下标共三步:获取key的hashCode值、高位运算、取模运算。
HashMap 底层的数组长度length总是2的n次方时, h & (length -1)运算等价于 h % length (对length 取模),但& 比 % 具有更高的效率。
在JDK1.8 中优化了高位运算的算法,具体的方式是hashCode的高16位 异或 低16位(即(h = key.hashCode())^ (h >>>16)
)。这么做可以在数组table的length较小的时候,也能保证考虑到高低位都参与到 Hash 的计算中,同时不会有太大的开销,下面具体说明,n 为table的长度:
4.3 构造方法
- 1、HashMap(int initialCapacity, float loadFactor)
// 指定“容量大小”和“加载因子”的构造函数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// HashMap的最大容量只能是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子不能小于0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//根据初始容量,计算出扩容临界值
this.threshold = tableSizeFor(initialCapacity);
}
- 2、HashMap(int initialCapacity)
// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 3、HashMap()
// 默认构造函数
public HashMap() {
// 设置“加载因子”为默认加载因子0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
在以上的三个构造方法中,常用的是无参构造(HashMap()
)方法,使用无参构造方法时,创建的HashMap初始容量为16,负载因子为0.75,图示:
- 4、HashMap(Map<? extends K, ? extends V> m)
// 包含“子Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将m中的全部元素逐个添加到HashMap中
putMapEntries(m, false);
}
4.4 添加元素
- 1、put(K key, V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* @param hash 根据静态方法hash获得的hash值
* @param key 键
* @param value 值
* @param onlyIfAbsent if true,当键相同时,不修改已存在的值
* @param evict if false, the table is in creation mode. 不用管
* @return previous value, or null if none 以前的value,或null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
//1.校验table是否为空或者length等于0,如果是则调用resize方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.通过hash值计算索引位置,将该索引位置的头节点赋值给p,如果p为空则
//直接在该索引位置新增一个节点即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//发生了哈希碰撞(也就是该数组下标上已经存在着其它节点),有以下几种情况
else {
//e 临时节点的作用, k 存放该当前节点的key
Node<K,V> e; K k;
// 3.判断p节点的key和hash值是否跟传入的相等,如果相等, 则p节点
//即为要查找的目标节点,将p节点赋值给e节点(第一种情况)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.判断p节点是否为TreeNode, 如果是则调用红黑树的putTreeVal方法
//查找目标节点(第二种情况,hash值不等于首节点)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5.走到这代表p节点为普通链表节点,则调用普通的链表方法进行
//查找,使用binCount统计链表的节点数(第三种情况,hash值不等于首节点)
//遍历链表
for (int binCount = 0; ; ++binCount) {
// 6.如果p的next节点为空时,则代表找不到目标节点,则新增一个节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 7.校验节点数是否超过8个,如果超过则调用treeifyBin方法
//将链表节点转为红黑树节点,
// 减一是因为循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
//把这个链表转化成红黑树,然后直接退出循环
treeifyBin(tab, hash);
break;
}
// 8.如果e节点的hash值和key值都与传入的相同,则e节点
//即为目标节点,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 将p指向下一个节点
p = e;
}
}
// 9.如果e节点不为空,则代表目标节点存在,使用传入的value覆盖
//该节点的value,并返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 10.如果插入节点后节点数超过阈值,则调用resize方法进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
借用一张图表示HashMap中添加元素的过程:
HashMap的put方法流程总结:
- 1、put(key, value)中直接调用了内部的putVal方法,并且先对key进行了hash操作【
put方法中计算key的hash只,并调用putVal方法
】; - 2、putVal方法中,先检查HashMap数据结构中的索引数组表是否位空,如果是的话则进行一次resize操作【
判断数组是否为空,为空就扩容
】; - 3、以HashMap索引数组表的长度减一与key的hash值进行与运算,得出在数组中的索引,如果索引指定的位置值为空,则新建一个k-v的新节点【
如果计算出来的位置没值,直接插入
】; - 4、如果不满足的3的条件,则说明索引指定的数组位置的已经存在内容,这个时候称之哈希碰撞【
如果计算出来的位置没值,代表发生了哈希碰撞
】; - 5、判断key索引到的节点(暂且称作被碰撞节点)的hash、key是否和当前待插入节点(新节点)的一致,如果是一致的话,则先保存记录下该节点;如果新旧节点的内容不一致时,则再看被碰撞节点是否是树类型,如果是树类型的话,则按照树的操作去追加新节点内容;如果被碰撞节点不是树类型,则说明当前发生的碰撞在链表中(此时链表尚未转为红黑树),此时进入一轮循环处理逻辑中【
判断发生哈希碰撞的位置是红黑树结构还是链表结构,如果是树类型的话,则按照树的操作去追加新节点,否则进入树结构处理逻辑
】; - 6、循环中,先判断被碰撞节点的后继节点是否为空,为空则将新节点作为后继节点,作为后继节点之后并判断当前链表长度是否超过最大允许链表长度8,如果大于的话,需要进行一轮是否转树的操作;如果在一开始后继节点不为空,则先判断后继节点是否与新节点相同,相同的话就记录并跳出循环;如果两个条件判断都满足则继续循环,直至进入某一个条件判断然后跳出循环【
挨个遍历链表上的Node的key和新插入节点的key是否相同,相同就替换value,否则追加。此过程中还要注意是否链表长度>8,大于的话,就调用treeifyBin
】; - 7、步骤8中转树的操作treeifyBin,如果map的索引表为空或者当前索引表长度还小于64(最大转红黑树的索引数组表长度),那么进行resize操作就行了;否则,就转换成红黑树,如果被碰撞节点不为空,那么就顺着被碰撞节点这条树往后新增该新节点【
treeifyBin方法的逻辑:如果数组长度>64才转换成红黑树,否则仅resize,再追加元素
】; - 8、最后,回到那个被记住的被碰撞节点,如果它不为空,默认情况下,新节点的值将会替换被碰撞节点的值,同时返回被碰撞节点的值(V)【
返回旧value值
】。 - 9、在上面判断流程走完之后,计算HashMap全局的modCount值,并判断当前元素数是否大于容量扩充的阈值,大于的话则进行一轮resize操作【
++modCount,再次判断是否需要扩容
】。
4.5 删除元素
- 1、remove
//移除某个节点
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab 哈希数组,p 待删除节点的Node,n 长度,index 当前数组下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 1.如果table不为空并且根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node 存储要删除的节点,e 临时变量,k 当前节点的key,v 当前节点的value
Node<K,V> node = null, e; K k; V v;
// 2.如果p的hash值和key都与入参的相同, 则p即为要删除的目标节点, 赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 3.否则将p.next赋值给e,向下遍历节点
else if ((e = p.next) != null) {
// 3.1 如果p是TreeNode则调用红黑树的方法查找节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 3.2 否则,进行普通链表节点的查找
else {
do {
// 当节点的hash值和key与传入的相同,则该节点即为目标节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e; // p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
} while ((e = e.next) != null); // e指向下一个节点
}
}
// 4.如果node不为空(即根据传入key和hash值查找到目标节点),则进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 4.1 如果是TreeNode则调用红黑树的移除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 4.2 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点,
// “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
else if (node == p)
tab[index] = node.next;
// 4.3 否则将node的上一个节点的next属性设置为node的next节点,
// 即将node节点移除, 将node的上下节点进行关联(链表的移除)
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
// 5.返回被移除的节点
return node;
}
}
return null;
}
4.6 获取元素
- 1、get(Object key)
//以key为条件,找到返回value。没找到返回null
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
//first 头结点,e 临时变量,n 长度,k key
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1.对table进行校验:table不为空 && table长度大于0 &&
// table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2.检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3.如果first不是目标节点,并且first的next节点不为空则继续遍历
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 4.如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 5.执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 6.找不到符合的返回空
return null;
}
4.7 是否包含某个key/value
即containsKey和containsValue方法:
public boolean containsKey(Object key)
{
//调用核心方法getNode,判断是否存在对应节点
return getNode(hash(key), key) != null;
}
//其实就是遍历Node<K,V>数组,看要查询的指定value是否在数组的value中
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;
}
4.8 扩容机制
//扩容
final Node<K,V>[] resize() {
//oldTab 为当前表的哈希桶
Node<K,V>[] oldTab = table;
//记录一下原来哈希数组的长度、临界值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
//初始化新哈希表的长度和临界值
int newCap, newThr = 0;
// 1.原数组的容量>0
if (oldCap > 0) {
// 1.1 判断原数组的容量是否超过最大容量值(1 << 30):如果超过则将阈值设置
//为Integer.MAX_VALUE,并直接返回原数组。
// 此时oldCap * 2比Integer.MAX_VALUE大,因此无法进行重新分布,只是单纯地
//将阈值扩容到最大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 1.2 否则,将newCap赋值为oldCap的2倍,如果newCap<最大容量并且oldCap>=16,
// 则将新阈值设置为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 2.如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
else if (oldThr > 0)
//那么新表的容量就等于旧的阈值
newCap = oldThr;
// 3.如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况
else {
//此时新表的容量为默认的容量 16
newCap = DEFAULT_INITIAL_CAPACITY;
//新的阈值为默认容量16 * 默认加载因子0.75f = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4.//如果新的阈值是0,对应的是 当前表是空的,但是有阈值的情况
if (newThr == 0) {
//根据新表容量 和 加载因子 求出新的阈值
float ft = (float)newCap * loadFactor;
//进行越界修复
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 5.//更新阈值
threshold = newThr;
//根据新的容量 构建新的哈希桶
@SuppressWarnings({
"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//更新哈希桶引用
table = newTab;
// 6.如果老数组不为空,则需遍历所有节点,将节点赋值给新数组
if (oldTab != null) {
//遍历老的哈希桶
for (int j = 0; j < oldCap; ++j) {
//当前的节点 e
Node<K,V> e;
//如果当前桶中有元素,则将链表赋值给e
if ((e = oldTab[j]) != null) {
//把已经赋值之后的变量置位null, 以便垃圾收集器回收空间
oldTab[j] = null;
//如果当前链表中就一个元素,(没有发生哈希碰撞)
if (e.next == null)
//直接将这个元素放置在新的哈希桶里。
//这里取下标 是用 哈希值 & 桶的长度-1 。
//由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
newTab[e.hash & (newCap - 1)] = e;
// 8.如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,
//依次放入新哈希桶对应下标位置
else {
//因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来
//的下标,即low位, 或者扩容后的下标,即high位。
//high位= low位+原哈希桶容量
//低位链表的头结点、尾节点
Node<K,V> loHead = null, loTail = null;
//高位链表的头节点、尾节点
Node<K,V> hiHead = null, hiTail = null;
//临时节点 存放e的下一个节点
Node<K,V> next;
do {
next = e.next;
// 9.1 利用哈希值 & 旧的容量,可以得到哈希值去模后,是大于等于
//oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,
//否则存放在高位
if ((e.hash & oldCap) == 0) {
// 如果loTail为空, 代表该节点为第一个节点
if (loTail == null)
// 则将loHead赋值为第一个节点
loHead = e;
else
// 否则将节点添加在loTail后面
loTail.next = e;
// 并将loTail赋值为新增的节点
loTail = e;
}
// 9.2 如果e的hash值与老数组的容量进行与运算为非0,则扩容后的
//索引位置为高位
else {
// 如果hiTail为空, 代表该节点为第一个节点
if (hiTail == null)
// 则将hiHead赋值为第一个节点
hiHead = e;
else
// 否则将节点添加在hiTail后面
hiTail.next = e;
// 并将hiTail赋值为新增的节点
hiTail = e;
}
// 并将hiTail赋值为新增的节点
} while ((e = next) != null);
// 10.将低位链表存放在原index处
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 11.将高位链表存放在新index处
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 12.返回新数组
return newTab;
}
- 1、在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
- 2、每次扩展的时候,都是扩展2倍;
- 3、扩展后Node对象的位置要么在原位置,要么移动到原始位置+增加的数组大小的位置。
在putVal()中,在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方。
在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。
扩容是一个特别耗性能的操作,所以当程序员在使用 HashMap,正确估算 map 的大小,初始化的时候给一个大致的数值,避免 map 进行频繁的扩容。
五、HashMap常见问题
5.1 HashMap的数组长度为什么一定是2的次幂
HashMap中根据key的哈希值计算下标的方式是tab[i = (n - 1) & hash]
,&为二进制中的与运算。
与运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
即:两位同时为“1”,结果才为“1”,否则为0。
因为hashMap 的数组长度都是2的n次幂 ,那么对于这个数再减去1,转换成二进制的话,就肯定是最高位为0,其他位全是1 的数。
以数组长度为8为例,那8-1 转成二进制的话,就是0111 。 那我们举一个随便的hashCode值,与0111进行与运算看看结果如何:
第一个key: hashcode值:10101001
& 0111
0001 (十进制为1)
-------------------------------------------
第二个key: hashcode值:11101000
& 0111
0000 (十进制为0)
--------------------------------------------
第三个key: hashcode值:11101110
& 0111
0110 (十进制为6)
这样得到的数,就会完整的得到原hashcode 值的低位值,不会受到与运算对数据的变化影响。
如果数组容量为7,7减去1转换成二进制是0110,此时再进行与运算:
第一个key: hashcode值:10101001
& 0110
0000 (十进制为0)
------------------------------------------
第二个key: hashcode值:11101000
& 0110
0000 (十进制为0)
--------------------------------------------
第三个key: hashcode值:11101110
& 0111
0110 (十进制为6)
可以看到,当数组长度不为2的n次幂 的时候,低位不同的hashCode 值与数组长度减一做与运算 的时候,会出现重复的数据。
因为当数组容量不为2的n次幂 的话,对应的-1所转换的二进制数肯定有一位为0 , 这样不管hashCode 值对应的该位,是0还是1 ,最终得到的该位上的数肯定是0,这带来的问题就是HashMap上的数组元素分布不均匀,而数组上的某些位置,永远也用不到
。
图示:
这将带来的问题就是你的HashMap 数组的利用率太低,并且链表可能因为上边的(n - 1) & hash 运算结果碰撞率过高,导致链表太深。所以说HashMap的长度一定是2的次幂,否则会出现性能问题。
5.2 HashMap在JDK1.7和JDK1.8中有哪些不同?【重要】
总的来说,JDK1.8版本的HashMap主要解决或优化了以下问题:
1>resize 扩容优化。
2>引入了红黑树,目的是避免单条链表过长而影响查询效率。
3>解决了多线程死循环问题,但仍是非线程安全的。
- 1、数据结构
JDK1.8之前采用的是拉链法(数组 + 链表
)。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(数组 + 链表 + 红黑树
)。 - **2、初始化方式 **
JDK1.7中的做法是:inflateTable() ;
JDK1.8中的做法是:直接集成到了扩容函数resize()中。 - 3、hash值计算方式
JDK1.7中的做法是:扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算。具体看下JDK1.7中哈希值的计算方式:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK1.8中的做法是:扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算。哈希值的计算方式:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 4、添加数据的规则
JDK1.7中的做法是:无冲突时,存放数组;冲突时,存放链表
。
JDK1.8中的做法是:无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
。 - 5、插入数据方式
JDK1.7中的做法是头插法
(先将原位置的数据移到后1位,再插入数据到该位置,这样能提高插put操作的性能,但在多线程情况下有链表逆序和循环链表的问题–会导致死循环)。
JDK1.8中的做法是尾插法
(直接插入到链表尾部或红黑树)。 - 6、扩容后存储位置的计算方式
JDK1.7中的做法是:全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))。
JDK1.8中的做法是:按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)。
5.3 HashMap是怎么解决哈希冲突的
5.3.1 什么是哈希?
简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同
。
5.3.2 什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希冲突)。
5.3.3 总结一下HashMap是使用了什么方法来有效解决哈希冲突
- 链表法
在哈希表中,每一个桶(bucket)或者槽(slot)都会对应一条链表,所有哈希值相同的元素放到相同槽位对应的链表中。
在插入的时候,我们可以通过散列函数计算出对应的散列槽位,将元素插入到对应的链表即可,时间复杂度为O(1);在查找或删除元素时,我们同样通过散列函数计算出对应的散列槽位,然后再通过遍历链表进行查找或删除,时间复杂度为O(k),k为链表长度。
5.4 能否使用任何类作为 Map 的 key?
可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:
- 如果类重写了 equals() 方法,也应该重写 hashCode() 方法,因为HashMap在计算key对应的哈希值时,用到了hashCode()方法。
- 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
- 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
- 用户自定义 Key 类最佳实践是使之为不可变的,不可变的类也可以确保 hashCode() 和
5.5 为什么HashMap中String、Integer这样的包装类适合作为Key?
这个问题可以接着上一个问题看。
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率。
- 1、 都是
final类型
,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况; - 2、 内部已
重写了equals()、hashCode()等方法
,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况。equals() 在未来不会改变,这样 hashCode() 值可以被缓存起来,拥有更好的性能。
5.6 HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
hashCode()方法返回的是int整数类型,其范围为-(2 31) 到 (2 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,因此导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置。
Hashtable
一、Hashtable概述
- 1、Hashtable的继承关系
Hashtable是线程安全的Map,其继承关系:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
从继承关系上看:
- Hashtable继承Dictionary,说明Hashtable是键值对形式类、并且键、值都不允许为null。
- 实现Map接口,说明支持Map的相关操作。
- 实现Cloneable接口,支持克隆。
- 实现Serializable接口,支持序列化。
- 2、Hashtable的底层结构
Hashtable的结构和HashMap类似:
二、Hashtable特点
HashMap和HashTable用法几乎一样,底层实现也几乎一样,但是HashTable的方法添加了synchronized关键字以确保线程安全,但同时也导致效率较低。两者的区别:
- 1、 HashMap中key和value均可以为null,但是
HashTable中key和value均不能为null
。 - 2、 HashMap采用的是数组(桶位)+链表+红黑树结构实现,而HashTable中采用的是数组(桶位)+链表实现。
- 3、
HashMap中出现hash冲突时,如果链表节点数小于8时是将新元素加入到链表的末尾,而hashtable中出现hash冲突时采用的是将新元素加入到链表的开头
。 - 4、 HashMap中数组容量的大小要求是2的n次方,如果初始化时不符合要求会进行调整,而HashTable中数组容量的大小可以为任意正整数。
- 5、 HashMap和HashTable通过hash值散列到hash表的算法不一样,Hashtable是古老的除留余数法,直接使用Object的hashcode,而后者是强制容量为2的幂,重新根据hashcode计算hash值,在使用hash和(hash表长度 – 1)进行与运算,也等价取模,但更加高效,取得的位置更加分散,偶数,奇数保证了都会分散到。前者就不能保证。
- 6、 HashMap不是线程安全的,而HashTable是线程安全的。
- 7、 HashMap中默认容量的大小是16,而HashTable中默认数组容量是11。
- 8、HashMap扩容时默认2;HashTable扩容方式是 old2+1。
- 9、HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。
三、Hashtable常见方法
- 1、构造一个空的散列表,默认初始容量(11)和负载因子(0.75)
public Hashtable()
- 2、构造一个空的散列表,具有指定的初始容量和默认负载因子(0.75)
public Hashtable(int initialCapacity)
- 3、构造一个空的散列表,具有指定的初始容量和指定的负载因子
public Hashtable(int initialCapacity, float loadFactor)
- 4、清空Hashtable
public synchronized void clear()
- 5、是否包含某个Value
public synchronized boolean contains(Object value)
- 6、是否包含某个key
public synchronized boolean containsKey(Object key)
- 7、是否包含某个Value
public boolean containsValue(Object value)
- 8、返回散列表中包含的键值对的Set视图
public Set<Map.Entry<K,V>> entrySet()
- 9、对Hashtable中的元素逐个操作
public synchronized void forEach(BiConsumer<? super K, ? super V> action)
- 10、获取key对应的value
public synchronized V get(Object key)
- 11、获取key对应的value,key不存在则返回默认值
public synchronized V getOrDefault(Object key, V defaultValue)
- 12、散列表是否为空
public synchronized boolean isEmpty()
- 13、返回散列表中包含的key的Set视图
public Set< K > keySet()
- 14、添加或替换键值对
public synchronized V put(K key, V value)
- 15、将指定Map中的键值对全添加到散列表中
public synchronized void putAll(Map<? extends K, ? extends V> t)
- 16、从散列表中删除键(及其对应的值)
public synchronized V remove(Object key)
- 17、如果指定键值对存在时,删除这个键值对
public synchronized boolean remove(Object key, Object value)
- 18、替换指定key对应的value
public synchronized V replace(K key, V value)
- 19、只有当特定的键值对存在时,替换value
public synchronized boolean replace(K key, V oldValue, V newValue)
- 20、返回此散列表中的键值对数
public synchronized int size()
- 21、返回此散列表中包含的value的集合
public Collection< V > values()
四、Hashtable源码分析
先看一些变量:
//保存key-value的数组,支持泛型
// Entry同样采用链表解决冲突,每一个Entry本质上是一个单向链表
private transient Entry<?,?>[] table;
//Hashtable中Entry对象的个数
private transient int count;
//临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
private int threshold;
//负载因子,当元素个数count大于总容量 * 负载因子时,扩容
private float loadFactor;
//Entry被改变的次数,用于fail-fast机制的实现
private transient int modCount = 0;
//最大容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
4.1 节点
Hashtable中Entry也和HashMap中的Entry相似,示例:
private static class Entry<K,V> implements Map.Entry<K,V> {
/**hash值*/
final int hash;
/**key表示键*/
final K key;
/**value表示值*/
V value;
/**节点下一个元素*/
Entry<K,V> next;
// 设置value。若value是null,则抛出异常。
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
// 覆盖equals()方法,判断两个Entry是否相等。
// 若两个Entry的key和value都相等,则认为它们相等。
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
//计算键值对的hashCode
public int hashCode() {
// "^" 按位异或, hash在调用构造器时传入
return hash ^ Objects.hashCode(value);
}
......
}
4.2 构造方法
- 1、Hashtable()
//无参构造方法
public Hashtable() {
//默认容量大小为11,负载因子设置为0.75
this(11, 0.75f);
}
- 2、Hashtable(int initialCapacity)
//带有初始化容量大小的构造方法
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
- 3、Hashtable(int initialCapacity, float loadFactor)
public Hashtable(int initialCapacity, float loadFactor) {
//检查参数的合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//如果设置初始容量为0,则默认修改为1
if (initialCapacity==0)
initialCapacity = 1;
//设置负载因子
this.loadFactor = loadFactor;
//根据设置的初始化容量创建数组
table = new Entry<?,?>[initialCapacity];
//散列表防止经过n次扩容后,数组大小可能会超出整数的最大值,所以
//这里设定一个上限的阈值
threshold = (int)Math.min(initialCapacity, MAX_ARRAY_SIZE + 1);
}
- 4、Hashtable(int initialCapacity, float loadFactor)
//使用Map集合初始化
public Hashtable(Map<? extends K, ? extends V> t) {
//根据集合元素的大小初始化数组容量
// 负载因子设置为0.75
this(Math.max(2*t.size(), 11), 0.75f);
//将集合t中元素全部存储
putAll(t);
}
public synchronized void putAll(Map<? extends K, ? extends V> t) {
// for循环遍历集合t,将t中元素存储到this集合中
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
//将键值对添加至集合中
put(e.getKey(), e.getValue());
}
从构造方法可以看出,Hashtable和HashMap的构造方法相同的是,均是对初始容量和加载因子完成了设置。不同的地方有2点:
- 1、HashMap对底层数组采取的懒加载,即当执行第一次插入时才会创建数组;而Hashtable在初始化时就创建了数组;
- 2、HashMap中数组的默认初始容量是16,并且必须的是2的指数倍数;而Hashtable中默认的初始容量是11,并且不要求必须是2的指数倍数。
4.3 获取元素
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//得到key的hashcode
int hash = key.hashCode();
//根据hashcode计算索引值
int index = (hash & 0x7FFFFFFF) % tab.length;
//根据index找到key对应Entry链表,遍历链表找到哈希值与键值均与key相同的元素
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//哈希值与key均一样,代表找到了指定key对应的元素
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
// 若没有找到,则返回null
return null;
}
简单总结get的步骤:
- 计算key对应的哈希值;
- 根据哈希值计算数组下标;
- 遍历数组下标位置对应的链表,找到则返回对应的value,否则返回null。
4.4 存入元素
public synchronized V put(K key, V value) {
// 检验数据值的合法性
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
//根据键值获取索引index
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//判断tab[index]是否已经有值
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//如果有值,则遍历
for(; entry != null ; entry = entry.next) {
//当前键值key已存在,更新key的映射值value,并返回旧值
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//若没有找到重复键值key,则将key和value添加链表末尾
addEntry(hash, key, value, index);
return null;
}
//添加元素
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判断当前数目是否超过阈值
if (count >= threshold) {
// 数目超过阈值,扩容,重排列
rehash();
//更新扩容后的数组信息
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 没有超过阈值,则添加至数组中
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
//增加元素数目
count++;
}
简单总结put的步骤:
- 根据key获取计算index;
- 如果index位置的链表中,key已存在,更新key的映射值value,并返回旧值;
- 否则将新的键值对添加到链表上。在这个过程中,需要判断需不需要扩容,如果扩容了,需要重新计算key新的哈希值及存储位置,再将键值对存放在新的存储位置上。
4.5 判断是否包含某个key/value
//判断是否含有value
public boolean containsValue(Object value) {
return contains(value);
}
public synchronized boolean contains(Object value) {
//检查参数的合法性,从这里也可以看出:Hashtable的value不允许为空,不然会报空指针
if (value == null) {
throw new NullPointerException();
}
// 双重for循环,外循环遍历数组,内循环遍历链表
Entry<?,?> tab[] = table;
//从后向前遍历数组
for (int i = tab.length ; i-- > 0 ;) {
//逐个遍历数组节点上的链表
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
// 判断是否包含键值key
public synchronized boolean containsKey(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
//计算数组的索引,Hashtable本质上采用除数取余法进行散列分布
int index = (hash & 0x7FFFFFFF) % tab.length;
// index定位数组位置,for遍历链表查找元素
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return true;
}
}
return false;
}
containsKey
方法中有行代码:if ((e.hash == hash) && e.key.equals(key))
,这里其实只判断e.key.equals(key)即可,但是考虑到执行的速度,可以先判断e.hash == hash,因为hash是提前计算好的值且该判断比e.key.equals(key)执行速度要快,而hashtable里大部分元素的hash值是不相同的,只有当hash值相同时才用equal判断,这样就可以快速筛选hashtable的元素。
4.6 替换元素
public synchronized V replace(K key, V value) {
Objects.requireNonNull(value);
//根据键值查找元素
Entry<?,?> tab[] = table;
//固定套路,根据key来计算哈希值,进而计算在数组中的位置index
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
//根据index找到在数组中的位置后,遍历该位置上的链表
for (; e != null; e = e.next) {
//查找成功,替换元素值
if ((e.hash == hash) && e.key.equals(key)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
return null;
}
4.7 删除元素
//根据键值删除元素,返回被删除元素值
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
//for遍历链表查找元素
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
//查找到元素进行链表的节点删除操作
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
//pre结点的next指针指向e的next,等价于e被删除
prev.next = e.next;
} else {
//否则,说明需要删除的为起始结点
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
4.8 获取value集合
public synchronized Enumeration<V> elements() {
return this.<V>getEnumeration(VALUES);
}
// 获取Hashtable的枚举类对象
// 若Hashtable的实际大小为0,则返回“空枚举类”对象;
// 否则,返回正常的Enumerator的对象。
private <T> Enumeration<T> getEnumeration(int type) {
if (count == 0) {
return Collections.emptyEnumeration();
} else {
return new Enumerator<>(type, false);
}
}
// 获取Hashtable的迭代器
// 若Hashtable的实际大小为0,则返回“空迭代器”对象;
// 否则,返回正常的Enumerator的对象。(Enumerator实现了迭代器和枚举两个接口)
private <T> Iterator<T> getIterator(int type) {
if (count == 0) {
return Collections.emptyIterator();
} else {
return new Enumerator<>(type, true);
}
}
// Enumerator的作用是提供了“通过elements()遍历Hashtable的接口” 和
//“通过entrySet()遍历Hashtable的接口”。
private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
// 指向Hashtable的table
Entry<?,?>[] table = Hashtable.this.table;
// Hashtable的总的大小
int index = table.length;
Entry<?,?> entry;
Entry<?,?> lastReturned;
int type;
// Enumerator是 “迭代器(Iterator)” 还是 “枚举类(Enumeration)”的标志
// iterator为true,表示它是迭代器;否则,是枚举类。
boolean iterator;
// 在将Enumerator当作迭代器使用时会用到,用来实现fail-fast机制
protected int expectedModCount = modCount;
Enumerator(int type, boolean iterator) {
this.type = type;
this.iterator = iterator;
}
// 从遍历table的数组的末尾向前查找,直到找到不为null的Entry。
public boolean hasMoreElements() {
Entry<?,?> e = entry;
int i = index;
Entry<?,?>[] t = table;
/* Use locals for faster loop iteration */
while (e == null && i > 0) {
e = t[--i];
}
entry = e;
index = i;
return e != null;
}
// 获取下一个元素
// 注意:从hasMoreElements() 和nextElement() 可以看出“Hashtable的elements()遍历方式”
// 首先,<span style="color:#ff0000;">从后向前的遍历table数组</span>。table数组的每个节点都是一个单向链表(Entry)。
// 然后,依次向后遍历单向链表Entry。
@SuppressWarnings("unchecked")
public T nextElement() {
Entry<?,?> et = entry;
int i = index;
Entry<?,?>[] t = table;
/* Use locals for faster loop iteration */
while (et == null && i > 0) {
et = t[--i];
}
entry = et;
index = i;
if (et != null) {
Entry<?,?> e = lastReturned = entry;
entry = e.next;
return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
}
throw new NoSuchElementException("Hashtable Enumerator");
}
// Iterator methods
public boolean hasNext() {
return hasMoreElements();
}
public T next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return nextElement();
}
public void remove() {
if (!iterator)
throw new UnsupportedOperationException();
if (lastReturned == null)
throw new IllegalStateException("Hashtable Enumerator");
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
synchronized(Hashtable.this) {
Entry<?,?>[] tab = Hashtable.this.table;
int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
for(Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
if (e == lastReturned) {
modCount++;
expectedModCount++;
if (prev == null)
tab[index] = e.next;
else
prev.next = e.next;
count--;
lastReturned = null;
return;
}
}
throw new ConcurrentModificationException();
}
}
}
4.9 扩容
//扩容方法
protected void rehash() {
//获取旧数组大小
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 创建新容量大小的Entry数组,数组容量大小为原数组的2倍+1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
//如果散列表的容量已经是MAX_ARRAY_SIZE,则不再扩容
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//重新计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//将原数组中元素拷贝至新数组
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
//重新计算新数组的索引值
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
//先插入起始值
e.next = (Entry<K,V>)newMap[index];
//对应的向右侧移动
newMap[index] = e;
}
}
}
LinkedHashMap
一、LinkedHashMap概述
- 1、LinkedHashMap的继承关系
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
图示:
从继承关系上看,LinkedHashMap继承了HashMap的特性,并且在其基础上扩展了一些新特性(元素的有序性)。
- 2、LinkedHashMap的底层结构
说LinkedHashMap之前,先说HashMap。HashMap是无序的,也就是说,迭代HashMap所得到的元素顺序并不是它们最初添加到HashMap的顺序。为了保证迭代元素的顺序与存入容器的顺序一致,诞生了HashMap的子类------LinkedHashMap。
由于LinkedHashMap是HashMap的子类,所以LinkedHashMap自然会拥有HashMap的所有特性。比如,LinkedHashMap也最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。
本质上,HashMap和双向链表合二为一即是LinkedHashMap。更准确地说,它是一个将所有Entry节点链入一个双向链表双向链表的HashMap。
关键的地方来了,在HashMap有一些空方法,比如:
void afterNodeAccess(Node<K,V> p) {
}
void afterNodeInsertion(boolean evict) {
}
void afterNodeRemoval(Node<K,V> p) {
}
LinkedHashMap重写了这些方法,这些方法被用来保持列表的有序。
关于LinkedHashMap和HashMap的差异,借用网上两张图来说明。
- HashMap的结构:
- LinkedHashMap的结构:
二、LinkedHashMap特点
- 1、由于继承HashMap类,所以默认初始容量是16,加载因子是0.75。
- 2、线程不安全。
- 3、具有fail-fast的特征。
- 4、底层使用双向链表,可以保存元素的插入顺序,顺序有两种方式:一种是按照插入顺序排序,一种按照访问做排序。默认以插入顺序排序。
- 5、key和value允许为null,key重复时,新value覆盖旧value。
- 6、可以用来实现LRU算法。
- 7、LinkedHashMap与HashMap的存取数据操作基本是一致的,只是增加了双向链表保证数据的有序性。
- 8、LinkedHashMap继承HashMap,基于HashMap+双向链表实现。(HashMap是数组+链表+红黑树实现的)。
三、LinkedHashMap方法
3.1 常见方法介绍
- 1、构造具有默认初始容量(16)和负载因子(0.75)的LinkedHashMap实例
public LinkedHashMap()
- 2、构造具有指定初始容量和默认负载因子(0.75)的LinkedHashMap实例
public LinkedHashMap(int initialCapacity)
- 3、构造具有指定初始容量和负载因子的LinkedHashMap实例
public LinkedHashMap(int initialCapacity, float loadFactor)
- 4、构造一个空的 LinkedHashMap实例,具有指定的初始容量,负载因子和订购模式(accessOrder)。accessOrder为false时,基于插入顺序;accessOrder为true时,基于访问顺序
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
- 5、清空元素
public void clear()
- 6、判断LinkedHashMap是否包含某个value
public boolean containsValue(Object value)
- 7、返回LinkedHashMap中的Entry形成的Set
public Set<Map.Entry<K,V>> entrySet()
- 8、对LinkedHashMap中的每个元素执行特定操作
public void forEach(BiConsumer<? super K, ? super V> action)
- 9、获取某个key对应的value
public V get(Object key)
- 10、获取某个key对应的value,该key不存在时,返回默认值
public V getOrDefault(Object key, V defaultValue)
- 11、返回LinkedHashMap中的key形成的Set
public Set<K> keySet()
- 12、返回value形成的集合
public Collection<V> values()
3.2 常见方法使用
//1.构造方法
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
LinkedHashMap<String, String> linkedHashMap1 = new LinkedHashMap<>(20);
LinkedHashMap<String, String> linkedHashMap2 = new LinkedHashMap<>(20,0.8f);
LinkedHashMap<String, String> linkedHashMap3 = new LinkedHashMap<>(20,0.8f,false);
//2.清空所有元素
linkedHashMap.clear();
System.out.println(linkedHashMap.size()); //0
linkedHashMap.put("1", "a");
linkedHashMap.put("2", "b");
linkedHashMap.put("3", "c");
linkedHashMap.put("4", "d");
//3.判断LinkedHashMap是否包含某个value
System.out.println(linkedHashMap.containsValue("d")); //true
//4.返回LinkedHashMap中的Entry形成的Set
Set set = linkedHashMap.entrySet();
System.out.println(set);//[1=a, 2=b, 3=c, 4=d]
//5.对LinkedHashMap中的每个元素执行特定操作
linkedHashMap.forEach((x,y) -> System.out.print("key:"+x+",value:"+y+" "));//key:1,value:a key:2,value:b key:3,value:c key:4,value:d
System.out.println();
//6.获取某个key对应的value
System.out.println(linkedHashMap.get("1")); //a
//7.获取某个key对应的value,该key不存在时,返回默认值
System.out.println(linkedHashMap.getOrDefault("5","e")); //e
//8.返回LinkedHashMap中的key形成的Set
Set<String> set2 = linkedHashMap.keySet();
System.out.println(set2); //[1, 2, 3, 4]
//9.对LinkedHashMap中的键值对执行特定操作,更新键值对
linkedHashMap.replaceAll((x,y) -> y+y);
System.out.println(linkedHashMap); //{1=aa, 2=bb, 3=cc, 4=dd}
//10.返回value形成的集合
Collection<String> collection = linkedHashMap.values();
System.out.println(collection); //[aa, bb, cc, dd]
四、LinkedHashMap源码解析
4.1 Entry
LinkedHashMap中存储的节点:
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
可以看出,LinkedHashMap中的节点Entry,相比于HashMap中的节点Node,多了指向前后位置的节点before和after,用于维护双向链表(即保持元素有序)。
HashMap与LinkedHashMap的Entry结构示意图:
LinkedHashMap中的变量:
private static final long serialVersionUID = 3801124242820219131L;
//头结点
transient LinkedHashMap.Entry<K,V> head;
//尾节点
transient LinkedHashMap.Entry<K,V> tail;
//这个变量决定链表元素的存储方式,false按照存储顺序存储,true表示按照访问顺序
//存储(将最近访问的元素移动到尾部),该变量和LRU算法相关
final boolean accessOrder;
accessOrder 默认为false,accessOrder 表示之后访问顺序按照元素的访问顺序进行,即不按照之前的插入顺序了,accessOrder为false表示按照插入顺序访问。
4.2 构造方法
- 1、LinkedHashMap(int initialCapacity, float loadFactor)
public LinkedHashMap(int initialCapacity, float loadFactor) {
//指定初始容量与负载因子
super(initialCapacity, loadFactor);
//默认访问顺序标识为false,表示按照存储顺序存储
accessOrder = false;
}
- 2、LinkedHashMap(int initialCapacity)
public LinkedHashMap(int initialCapacity) {
//指定初始容量,负载因子为0.75
super(initialCapacity);
//默认访问顺序标识为false,表示按照存储顺序存储
accessOrder = false;
}
- 3、LinkedHashMap()
public LinkedHashMap() {
//默认初始容量(16)和负载因子(0.75)
super();
//默认访问顺序标识为false,表示按照存储顺序存储
accessOrder = false;
}
- 5、LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
//指定初始容量与负载因子
super(initialCapacity, loadFactor);
//指定默认访问顺序标识
this.accessOrder = accessOrder;
}
4.3 是否包含某个value
public boolean containsValue(Object value) {
//从向前后进行遍历
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
4.4 获取元素
- 1、get(Object key)
public V get(Object key) {
Node<K,V> e;
//第一步是直接使用Hashmap中的函数getNode方法,获取value,如果为null,返回null
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
//将添加的元素移动到链表的尾端
afterNodeAccess(e);
return e.value;
}
//getNode方法来自于其父类HashMap
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) {
//如果此key对应的hash桶不为空执行以下
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对应的value并且返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
//将访问的这个元素移动到双向链表的尾端,并且将他的后面的元素向前移动
void afterNodeAccess(Node<K,V> e) {
//last为原链表的尾节点
LinkedHashMapEntry<K,V> last;
//如果accessOrder为true,并且尾端元素不是需要访问的元素
if (accessOrder && (last = tail) != e) {
//将节点e强制转换成linkedHashMapEntry,b为这个节点的前一个
//节点,a为它的后一个节点
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//p为最后一个元素,那么他的后置节点必定是空
p.after = null;
//b为e的前置元素,如果b为空,说明此元素必定是链表的第一个元素,更新之后
//链表的头结点已经变成尾节点,那么原链表的第二个节点就要变为头结点
if (b == null)
head = a;
else
//如果b不是空,那么b的后置节点就由p变为p的后置节点
b.after = a;
//如果p的后置节点不为空,那么更新后置节点a的前置节点为b
if (a != null)
a.before = b;
else
//如果p的后置节点为空,那么p就是尾节点,那么更新last的节点为p的前置节点
last = b;
//如果原来的尾节点为空,那么原链表就只有一个元素
if (last == null)
head = p;
else {
//更新当前节点p的前置节点为 原尾节点last, last的后置节点是p
p.before = last;
last.after = p;
}
//p为最新的尾节点
tail = p;
++modCount;
}
}
这里需要注意两点,一是调用次函数之后,访问的这个元素会移动到双向链表的尾端,二是在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询(get)访问数据,也会导致fail-fast,因为迭代的顺序已经改变。
- 2、getOrDefault
//实现与get方法基本类似,不过方法最后的返回值是默认值
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
//将添加的元素移动到链表的尾端
afterNodeAccess(e);
return e.value;
}
4.5 清空LinkedHashMap
public void clear() {
super.clear();
//将头尾节点都置为null
head = tail = null;
}
//HashMap的clear方法
public void clear() {
Node<K,V>[] tab;
modCount++;
//数组中所有元素都置为null
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
4.6 获取key组成的Set
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
//在这里linkedHashMap重写了此函数。
ks = new LinkedKeySet();
keySet = ks;
}
return ks;
}
final class LinkedKeySet extends AbstractSet<K> {
//链表元素个数
public final int size() {
return size; }
public final void clear() {
LinkedHashMap.this.clear(); }
//遍历的目的是获取Iterator
public final Iterator<K> iterator() {
return new LinkedKeyIterator();
}
//这个直接使用的hashmap的containsKey函数,最终使用的是getNode,前文已经讲过就不在强调
public final boolean contains(Object o) {
return containsKey(o); }
//remove最后也是调用了removeNode,前文已经讲过。就不在强调
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
}
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() {
return nextNode().getKey(); }
}
4.7 获取value组成的Collection
public Collection<V> values() {
Collection<V> vs;
return (vs = values) == null ? (values = new LinkedValues()) : vs;
}
final class LinkedValues extends AbstractCollection<V> {
public final int size() {
return size; }
public final void clear() {
LinkedHashMap.this.clear(); }
public final Iterator<V> iterator() {
return new LinkedValueIterator();
}
//linkedHashMap重写了contains函数
public final boolean contains(Object o) {
return containsValue(o); }
//省略了一些方法
}
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<V> {
public final V next() {
return nextNode().value; }
}
4.8 获取entry组成的Set
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() {
return size; }
public final void clear() {
LinkedHashMap.this.clear(); }
//之后的遍历都是通过Iterator来进行
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
//通过getnode方法来获取节点
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() {
return nextNode(); }
}
4.10 LinkedHashMap中的核心迭代器
abstract class LinkedHashIterator {
//下一个节点
LinkedHashMapEntry<K,V> next;
//当前节点
LinkedHashMapEntry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
//初始化的时候将next指向双向链表的头结点
next = head;
expectedModCount = modCount;
//current为空
current = null;
}
public final boolean hasNext() {
return next != null;
}
//nextNode方法就是我们用到的next方法,
//迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出
final LinkedHashMapEntry<K,V> nextNode() {
//记录要返回的e
LinkedHashMapEntry<K,V> e = next;
//fail-fast判断
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果返回的是空,则抛出异常
if (e == null)
throw new NoSuchElementException();
//更新当前节点为e
current = e;
//更新下一个节点是e的后置节点
next = e.after;
return e;
}
//删除方法,就直接使用了hashmap的remove
public final void remove() {
Node<K,V> 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;
}
}
LinkedHashMap只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像LinkedList那样进行双向访问。
在新增节点时,已经维护了元素之间的插入顺序了,所以在迭代访问时只需要不断的访问当前节点的下一个节点即可。
4.11 添加元素
由于LinkedHashMap是HashMap的子类,添加元素是通过调用父类的putVal来完成的。在HashMap的putVal方法中,有调用newNode(hash, key, value, null)
方法来创建节点的操作。
LinkedHashMap通过newNode/newTreeNode方法进行节点新增。
LinkedHashMap重写的newNode方法:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 作用:将新建的节点添加到维护的双向链表上去
// 方式:往链表的尾部进行添加
linkNodeLast(p);
return p;
}
//在双向链表的尾部添加新建的节点
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
// p为新的需要追加的结点
tail = p;
// 如果last为null.则表示现在链表为空。新new出来的p元素就是链表的头结点
if (last == null)
head = p;
// 否则就是链表中已存在结点的情况:往尾部添加即可
else {
// 把新追加p的结点的前驱结点设置之前的尾部结点
// 把之前的尾部结点的后驱结点设为新追加的p结点
p.before = last;
last.after = p;
}
}
可以看出LinkedHashMap的在新建一个结点的时候,做了两件事:
1.新建结点,并放入到对应的hash桶位置。
2.将新建的结点追加到双向链表的尾部。
结合构造方法来看的话:LinkedHashMap初始化时,accessOrder为false,就会按照插入顺序提供访问,插入方法使用的是父类HashMap的put方法,不过覆写了put方法,执行中调用的是newNode/newTreeNode和afterNodeAccess 方法。
LinkedHashMap通过新增头节点、尾节点,给每个节点增加before、after 属性。每次新增时,都把节点追加到尾节点,这样就可以保证新增节点是按照顺序插入到链表中的。
LinkedHashMap的没有自己的put方法的实现,而是使用父类HashMap的put方法:在正常新增之后,调用afterNodeAccess(e)和 afterNodeInsertion(evict)让LinkedHashMap自己做后续处理:
//回调函数,新节点插入之后回调 , 根据evict判断是否需要删除最老插入的节点。
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
//LinkedHashMap 默认返回false 则不删除节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//移动访问的节点到链表末端(该方法在get小节已详细介绍过)
void afterNodeAccess(Node<K,V> e) {
//省略代码....
}
//LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回true
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
4.12 删除元素
LinkedHashMap删除元素时,也是通过调用父类的方法来实现的。LinkedHashMap在删除元素时,会调用到自己重写的afterNodeRemoval方法:
//在删除节点e时,同步将e从双向链表上删除
void afterNodeRemoval(Node<K,V> e) {
// unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//待删除节点 p 的前置后置节点都置空
p.before = p.after = null;
//如果前置节点是null,则现在的头结点应该是后置节点a
if (b == null)
head = a;
else//否则将前置节点b的后置节点指向a
b.after = a;
//同理如果后置节点时null ,则尾节点应是b
if (a == null)
tail = b;
else//否则更新后置节点a的前置节点为b
a.before = b;
}