Java集合源码分析(八)——WeakHashMap
简介
WeakHashMap 继承于AbstractMap,实现了Map接口。
和HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。
不一样的是,JDK1.8开始,HashMap中引入了红黑树,节点名从entry改成了node,而WeakHashMap还是没有被修改,还是采用链表形式的拉链法解决哈希冲突。
所谓weak,就是WeakHashMap中存储的键值是弱引用的,是很有可能被GC回收的,所以,WeakHashMap中需要对被GC的键的键值对进行清除,其实现原理:
- WeakHashMap中有一个ReferenceQueue用来存储被GC回收的弱键;
- 当每次操作WeakHashMap的时候,就会需要同步table和queue,通过同步的行为,就可以删除table中已经被回收了的键的键值对。
源码分析
定义
public class WeakHashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V> {}
字段
// 默认初始容量,和hashmap一样
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 存储键值对链表头节点的数组
Entry<K,V>[] table;
// 当前节点数量
private int size;
// 扩容阈值
private int threshold;
// 加因子实际大小
private final float loadFactor;
// 被垃圾回收的弱引用键队列
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 修改次数
int modCount;
和参数和HashMap大致相同,不同的是,多了一个引用队列,用来存储被GC的引用,用于之后的同步。
构造函数
// 初始化容量和加载因子的构造函数
public WeakHashMap(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);
int capacity = 1;
// 通过比较位移的方式,得到第一个大于等于设定容量的2的幂次的合法容量
while (capacity < initialCapacity)
capacity <<= 1;
// 这个newtbale就是初始化了一个capactiy大小的空数组
table = newTable(capacity);
this.loadFactor = loadFactor;
// 计算扩容阈值
threshold = (int)(capacity * loadFactor);
}
// 初始化容量的构造
public WeakHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 默认构造
public WeakHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
// 添加其他map的构造
public WeakHashMap(Map<? extends K, ? extends V> m) {
// 设定容量和加载因子
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR);
// 把节点都添加进去
putAll(m);
}
内部类
1.节点的结构
// 继承了弱引用,实现了Map.Entry,所以它的节点键值都是弱引用,不会防止GC
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
// 节点存储值
V value;
// 节点哈希值
final int hash;
// 下一个节点引用
Entry<K,V> next;
// 构造,新建节点
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}
@SuppressWarnings("unchecked")
public K getKey() {
return (K) WeakHashMap.unmaskNull(get());
}
public V getValue() {
return value;
}
public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写了比较接口函数,就比较类型和键值
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
K k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
V v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 重写了hashCode函数,然会键值的哈希值而不是对象的哈希值
public int hashCode() {
K k = getKey();
V v = getValue();
return Objects.hashCode(k) ^ Objects.hashCode(v);
}
public String toString() {
return getKey() + "=" + getValue();
}
}
2.迭代器
private abstract class HashIterator<T> implements Iterator<T> {
// 当前索引
private int index;
// 当前元素
private Entry<K,V> entry;
// 上一次返回的元素
private Entry<K,V> lastReturned;
// 实现fast-faiul机制
private int expectedModCount = modCount;
// 指向下一个键值(强引用)
private Object nextKey;
// 当前节点(强引用)
private Object currentKey;
// 构造
HashIterator() {
index = isEmpty() ? 0 : table.length;
}
// 判断是否存在下一个节点
public boolean hasNext() {
Entry<K,V>[] t = table;
// 如果下一个而节点是空的,就需要遍历table,将下一个节点指向table中下一个不为空的头节点
while (nextKey == null) {
Entry<K,V> e = entry;
int i = index;
while (e == null && i > 0)
e = t[--i];
entry = e;
index = i;
if (e == null) {
currentKey = null;
return false;
}
nextKey = e.get(); // hold on to key in strong ref
if (nextKey == null)
entry = entry.next;
}
return true;
}
// 获取下一个节点
protected Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (nextKey == null && !hasNext())
throw new NoSuchElementException();
lastReturned = entry;
entry = entry.next;
currentKey = nextKey;
nextKey = null;
return lastReturned;
}
// 删除当前节点
public void remove() {
if (lastReturned == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
WeakHashMap.this.remove(currentKey);
expectedModCount = modCount;
lastReturned = null;
currentKey = null;
}
}
// 值遍历
private class ValueIterator extends HashIterator<V> {
public V next() {
return nextEntry().value;
}
}
// 键的遍历
private class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
// 键值对的遍历
private class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
3.集合
// 键的集合
private class KeySet extends AbstractSet<K> {
// 调用迭代器的接口
public Iterator<K> iterator() {
return new KeyIterator();
}
public int size() {
return WeakHashMap.this.size();
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
if (containsKey(o)) {
WeakHashMap.this.remove(o);
return true;
}
else
return false;
}
public void clear() {
WeakHashMap.this.clear();
}
// 分割迭代器,用于并行
public Spliterator<K> spliterator() {
return new KeySpliterator<>(WeakHashMap.this, 0, -1, 0, 0);
}
}
// 键值对集合
private class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
Entry<K,V> candidate = getEntry(e.getKey());
return candidate != null && candidate.equals(e);
}
public boolean remove(Object o) {
return removeMapping(o);
}
public int size() {
return WeakHashMap.this.size();
}
public void clear() {
WeakHashMap.this.clear();
}
// 深拷贝接口
private List<Map.Entry<K,V>> deepCopy() {
List<Map.Entry<K,V>> list = new ArrayList<>(size());
// 将键值对都添加到新的链表当中
for (Map.Entry<K,V> e : this)
list.add(new AbstractMap.SimpleEntry<>(e));
return list;
}
// 转化为数组
public Object[] toArray() {
return deepCopy().toArray();
}
// 模板方法的数组
public <T> T[] toArray(T[] a) {
return deepCopy().toArray(a);
}
// 分割迭代
public Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(WeakHashMap.this, 0, -1, 0, 0);
}
}
方法
1.哈希函数
// 获取键的哈希值
final int hash(Object k) {
int h = k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
// 这样搞是为了尽可能地均匀吧
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
2.元素获取
// 获取最新的表
private Entry<K,V>[] getTable() {
// 之所以要获取最新的表,是因为需要先删除GC的Key
expungeStaleEntries();
return table;
}
// 获取对应键的元素值
public V get(Object key) {
// 如果key是null那么就用一个final的空对象,这样保证每次null的对象相同
Object k = maskNull(key);
// 获取key的哈希值
int h = hash(k);
// 获取最新的表,在这里会触发一次表的更新,就是将GC了的key给移除
Entry<K,V>[] tab = getTable();
// 根据哈希值获取当前table中对应的索引
int index = indexFor(h, tab.length);
// 拿出节点
Entry<K,V> e = tab[index];
// 遍历链表
while (e != null) {
// 匹配值
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
// 没有找到就返回空
return null;
}
3.元素添加
// 添加获取修改键值
public V put(K key, V value) {
// 这些操作和上面差不多
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
// 遍历链表,如果有相同的key,那就直接修改值
for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
if (h == e.hash && eq(k, e.get())) {
V oldValue = e.value;
if (value != oldValue)
e.value = value;
return oldValue;
}
}
modCount++;
Entry<K,V> e = tab[i];
// 数组头添加新的节点,采用了头插法
tab[i] = new Entry<>(k, value, queue, h, e);
if (++size >= threshold)
// 如果当数量大于等于阈值则进行扩容
resize(tab.length * 2);
return null;
}
4.删除被GC的节点
WeakHashTable就是通过这个函数实现弱引用被GC后的表中节点的回收。
private void expungeStaleEntries() {
// 遍历引用队列中被标记回收得值
for (Object x; (x = queue.poll()) != null; ) {
// 获取锁,防止其他线程进入
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
// 删除节点
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // 将键对应得值指向空,这样就可以让GC来回收原来得对象
size--;
break;
}
prev = p;
p = next;
}
}
}
}
5.扩容
扩容的大致其实和HashMap差不多
// 扩容到新得容量
void resize(int newCapacity) {
Entry<K,V>[] oldTable = getTable();
int oldCapacity = oldTable.length;
// 边界判断
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 新建一个标准大小的数组
Entry<K,V>[] newTable = newTable(newCapacity);
// 将旧数组上的数据复制过去
transfer(oldTable, newTable);
// 更新引用
table = newTable;
// 查看size是不是大于,扩容阈值的一半,如果不是,说明size又变小了,不需要扩容了
if (size >= threshold / 2) {
// 更新扩容阈值
threshold = (int)(newCapacity * loadFactor);
} else {
// 更新GC后的key
expungeStaleEntries();
// 返回原有大小的表
transfer(newTable, oldTable);
table = oldTable;
}
}
// 将原表复制到目标表
private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
// 遍历原表
for (int j = 0; j < src.length; ++j) {
Entry<K,V> e = src[j];
src[j] = null;
// 遍历链表,再将节点放到新表的对应位置
while (e != null) {
Entry<K,V> next = e.next;
Object key = e.get();
if (key == null) {
// 用于GC
e.next = null;
e.value = null;
size--;
} else {
// 获取到对应的索引
int i = indexFor(e.hash, dest.length);
e.next = dest[i];
dest[i] = e;
}
e = next;
}
}
}
6.元素删除
public V remove(Object key) {
// 同上
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
Entry<K,V> prev = tab[i];
Entry<K,V> e = prev;
// 遍历链表
while (e != null) {
Entry<K,V> next = e.next;
// 匹配到了就删除
if (h == e.hash && eq(k, e.get())) {
modCount++;
size--;
// 如果是头节点
if (prev == e)
tab[i] = next;
else
prev.next = next;
return e.value;
}
prev = e;
e = next;
}
return null;
}
总结
源码总结
- 大致的1.7的哈希表差不多,采用拉链法解决哈希冲突,只有链表,采用头插法,包括初始容量、扩容阈值和大小。
- 表中的节点继承了弱引用,这说明它的引用的键是会被垃圾回收的。
- 主要的区别就是它再对表进行修改的时候,都会调用expungeStaleEntries函数,用来删除那些已经被垃圾回收了的键,所对应的键值对。需要删除的键会存放在ReferenceQueue 中,每次去获取需要被删除的key。
- 和其他集合的重要区别,WeakHashMap没有实现克隆和序列化的接口。
Java集合源码分析(八)——WeakHashMap的更多相关文章
- java集合源码分析(三):ArrayList
概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...
- java集合源码分析(六):HashMap
概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...
- Java 集合源码分析(一)HashMap
目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...
- Java集合源码分析(三)LinkedList
LinkedList简介 LinkedList是基于双向循环链表(从源码中可以很容易看出)实现的,除了可以当做链表来操作外,它还可以当做栈.队列和双端队列来使用. LinkedList同样是非线程安全 ...
- Java集合源码分析(四)Vector<E>
Vector<E>简介 Vector也是基于数组实现的,是一个动态数组,其容量能自动增长. Vector是JDK1.0引入了,它的很多实现方法都加入了同步语句,因此是线程安全的(其实也只是 ...
- Java集合源码分析(二)ArrayList
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- java集合源码分析几篇文章
java集合源码解析https://blog.csdn.net/ns_code/article/category/2362915
- Java集合源码分析(六)TreeSet<E>
TreeSet简介 TreeSet 是一个有序的集合,它的作用是提供有序的Set集合.它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, j ...
- Java集合源码分析(五)HashSet<E>
HashSet简介 HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持.它不保证set 的迭代顺序:特别是它不保证该顺序恒久不变.此类允许使用null元素. HashSet源 ...
随机推荐
- Java 类型信息详解和反射机制
本文部分摘自 On Java 8 RTTI RTTI(RunTime Type Information)运行时类型信息,能够在程序运行时发现和使用类型信息,把我们从只能在编译期知晓类型信息并操作的局限 ...
- Ceph 状态报警告 pool rbd has many more objects per pg than average (too few pgs?)
定位问题 [root@lab8106 ~]# ceph -s cluster fa7ec1a1-662a-4ba3-b478-7cb570482b62 health HEALTH_WARN pool ...
- EDI在服装行业的应用
EDI发展迅速,从最初应用于汽车.物流.零售等行业开始,应用范围不断扩大.当下金融行业.服装行业也加入到使用EDI进行数据传输的队伍中.本文主要介绍服装行业通过EDI系统实现业务数据收发,本次EDI项 ...
- Tarjan算法求割点
(声明:以下图片来源于网络) Tarjan算法求出割点个数 首先来了解什么是连通图 在图论中,连通图基于连通的概念.在一个无向图 G 中,若从顶点i到顶点j有路径相连(当然从j到i也一定有路径),则称 ...
- [原题复现+审计][BJDCTF2020]Mark loves cat($$导致的变量覆盖问题)
简介 原题复现:https://gitee.com/xiaohua1998/BJDCTF2020_January 考察知识点:$$导致的变量覆盖问题 线上平台:https://buuoj.cn( ...
- cmd编译java代码为什么总是说找不到main方法;请园子里大神指点迷津!!!
编写源代码如下: cmd,编译路径:E: cd Notepad cd src javac Character.java jvav Character 运行结果: 实在是找不到问题点,请评论区给予指导啊 ...
- IntelliJ IDEA 2020.2.3永久破解激活教程 - 2020.10.27
申明:本教程 IntelliJ IDEA 破解补丁.激活码均收集于网络,请勿商用,仅供个人学习使用,如有侵权,请联系作者删除 不花钱 的方式 IDEA 2020.2 激活到 2089 年 注意:教程适 ...
- 使用pdfFactory为PDF文件设定查看选项
一般情况下,大部分PDF文件都会按照默认的查看设置,以100%的尺寸显示第一页的内容.但在一些特殊情况下,PDF文件的创建者会设定其他的文件查看尺寸,或设定打开页为第N页,来达到引起阅读者关注的目的. ...
- js 表格上checkbox 全选
<table class="layui-table"> <thead> <tr> <th width="75"> ...
- centos克隆虚拟机
首先我们把所要克隆的虚拟机关机,然后在所要克隆的虚拟机上右键,选择管理,选择克隆,出现如下界面: 点击下一步,选择虚拟机的当前状态,继续点击下一步 接着选择创建完整克隆,选择这个时,表明克隆出来的这个 ...