(部分图片来源于cyc作者)
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。
ArrayList:基于动态数组实现,支持随机访问。
Vector:和 ArrayList 类似,但它是线程安全的。
LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
LinkedList:可以用它来实现双向队列。
PriorityQueue:基于堆结构实现,可以用它来实现优先队列。
HashMap:基于哈希表实现。
HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
System.out.println(item);
}
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
@SafeVarargs
public static <T> List<T> asList(T... a)
应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
Integer[] arr = {
1, 2, 3};
List list = Arrays.asList(arr);
也可以使用以下方式调用 asList():
List list = Arrays.asList(1, 2, 3);
ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。数组的默认大小为 10。
//默认容量是10
private static final int DEFAULT_CAPACITY = 10;
//说明调用的是有参构造器,初始容量就是自己传的initialCapacity的值
private static final Object[] EMPTY_ELEMENTDATA = {
};
//说明调的是无参构造器,默认容量是10(第1次添加元素时初始化容量为10)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {
};
transient Object[] elementData; // 实际存放元素的地方,transient该关键字声明数组默认不会被序列化。
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1)
,即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍左右。(oldCapacity 为偶数就是 1.5 倍,为奇数就是1.5倍-0.5)
扩容操作需要调用 Arrays.copyOf()
把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
//添加
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity); //如果是通过无参方式构造的 就直接把默认值(10)返回
}
return minCapacity; //否则就返回minCapacity
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。并发修改异常,而Vector是线程安全的 就不需要modCount
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//开始进行扩容
private void grow(int minCapacity) {
int oldCapacity = elementData.length;//数组原长度
int newCapacity = oldCapacity + (oldCapacity >> 1); //新长度=10+5=15 右移操作就是除2
if (newCapacity - minCapacity < 0) //如果新长度比需要的最小长度还小的话
newCapacity = minCapacity; //就把最小长度给新长度
if (newCapacity - MAX_ARRAY_SIZE > 0) //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 如果新长度比int最大值还大
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
*/
删除元素:
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
ArrayList它里面的clone()方法是深拷贝
假设B复制了A,修改A的时候,看B是否发生变化:
浅拷贝只是增加了一个指针指向已存在的内存地址
深拷贝是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,跟原对象是相互独立的
ArrayList的subList方法:subList方法只是保存了下标,新集合和原集合共用一个引用:
ArrayList<String> list = new ArrayList<>();
list.add("e");
list.add("1");
list.add("2");
list.add("3");
List<String> strings = list.subList(0, 3);
list.set(0,"6666");
System.out.println(strings .get(0));//打印 6666 说明subList方法只是保存了下标,新集合和原集合共用一个引用
Arrays.asList方法:
Integer[] arr = {
1, 2, 3};
//注意asList() 的参数不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
//因为引用类型的数组,a=[] 基本类型数组:a= [long[] 会把他变成二维数组.
List list1 = Arrays.asList(arr);
System.out.println(list1.size());// 1 因为是基本类型的数组,如果把int换成Integer,则size=3
Fail-Fast机制:
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小。在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException,即并发修改异常
**Vector同步:**它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。
Vector与 ArrayList 的比较
替代方案:可以使用 Collections.synchronizedList();
得到一个线程安全的 ArrayList。
List<String> synList = Collections.synchronizedList(new ArrayList<>());
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
List<String> list = new CopyOnWriteArrayList<>();
基于双向链表实现,使用 Node 存储链表节点信息。
ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:
参考地址:https://joonwhee.blog.csdn.net/article/details/78996181?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control&dist_request_id=1328642.24557.16156230262267211&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.control和https://gitee.com/gu_chun_bo/java-construct/blob/d466640dc977a61433fecbbb069ffe7a56d46d1a/java%E9%9B%86%E5%90%88/HashMap.md
面试环节:https://blog.csdn.net/v123411739/article/details/106324537?spm=1001.2014.3001.5502
HashMap底层结构: 数组+链表+红黑树 JDK8是尾插法
内部包含了一个 Node
//无参构造的时候,默认初始化容量为 2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量 2^32
static final float DEFAULT_LOAD_FACTOR = 0.75f; //无参构造的时候,默认负载因子
//树形化阈值为8 即当链表的长度大于8的时候,会将链表转为红黑树,优化查询效率。链表查询的时间复杂度为o(n) , 红黑树查询的时间复杂度为 o(log n)
static final int TREEIFY_THRESHOLD = 8;
//解除树形化阈值,就是当红黑树的节点个数小于等于6时,会将红黑树结构转为链表结构。
static final int UNTREEIFY_THRESHOLD = 6;
/*树形化的最小容量为64;前面我们看到有一个树形化阈值,就是当链表的长度大于8的时候,会从链表转为红黑树,其实不一定是这样的。转为红黑树有两个条件:
① 链表的长度大于8
② HashMap数组的容量大于等于64 需要两个条件都成立的情况下,链表结构才会转为红黑树,否则还是扩容。*/
static final int MIN_TREEIFY_CAPACITY = 64;
//hash表什么时候初始化? 在第一次插入值的时候才初始化
transient Node<K,V>[] table;
transient int size;//当前hash表中的元素个数
transient int modCount;//当前hash表的结构修改次数,这个就是快速失败的机制
//扩容阈值,也就是初始化的大小 当hash表中的元素超过阈值 就会触发扩容
int threshold;
final float loadFactor;//负载因子 使用默认的0.75就行
public HashMap(int initialCapacity, float loadFactor) {
//下面三个if主要是进行合法校验
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); //如果传入的容量大小不是2的次方,就会找传入值最近的2的次方
}
//返回一个大于等于当前值cap的一个数字,并且这个数字一定是2的次方数,比如传10,就会返回16。
static final int tableSizeFor(int cap) {
int n = cap - 1;//为什么减1呢?如果传入16,则会返回32,变为了2倍浪费了很大的空间
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;
}
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么计算得到的 hash 值总是相同的。我们首先想到的就是把 hash 值对 table 长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是模运算消耗还是比较大的,所以就改为了位运算,将高位也参与计算,目的是为了降低 hash 冲突的概率。如下代码:
// 计算key的hash值
static final int hash(Object key) {
int h;
//如果键为null,就直接存到index为0的位置,否则先拿到key的hashCode值,再将hashCode的高16位参与运算,作用就是让key的hash值的高16位也参与路由运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
int n = tab.length;//获得数组的长度
int index = (n - 1) & hash;// 将(tab.length - 1) 与 hash值进行&运算,获取桶的下标
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//首先来看hash(key)方法,通过计算返回一个hash值
static final int hash(Object key) {
int h;
//如果键为null,就直接存到index为0的位置,否则就让hash值和它右移16位之后的值进行异或运算,作用就是让key的hash值的高16位也参与路由运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//tab:引用当前hashMap的散列表 p: 表示当前散列表的元素 n:散列表数组的长度 i: 表示路由寻址的下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
//延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//i = (n - 1) & hash 这个就是确定桶的下标的
//如果当前桶下标的值为null,说明还没有存值,直接封装为一个Node节点放在该位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//e: nodel临时元素 k: 表示临时的一个key
Node<K,V> e; K k;
//如果插入的节点与当前节点的key一样,则更新value值就行
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//前面不满足 说明key不一样,则如果是树结构的话,就根据红黑树的插入规则进行插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//前面 不是树,那就只有是链表
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
//如果当前结点的下一个结点是null,就将key value 进行封装 插入到末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//插入完成之后 判断是否达到了树的阈值,如果达到了,就要进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果在遍历链表的时候 找到了一个key一样的 就更新这个节点的value值
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;
//插入新元素之后,size+1,如果达到了扩容阈值,就会触发扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
为什么需要扩容?缓解hash冲突导致的链表过长的问题,缓解查询效率:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //引用扩容之前的表
int oldCap = (oldTab == null) ? 0 : oldTab.length; //获得扩容之前数组的长度
int oldThr = threshold; //获得扩容之前的阈值
int newCap, newThr = 0; //newCap:扩容之后数组的大小 newThr: 扩容之后,再次触发扩容的条件
//条件老表长度>0 说明hashMap中的散列表数组已经初始化过了,此次是正常扩容
if (oldCap > 0) {
//如果扩容之前的数组长度已经大于了可设置的最大值,则不扩容,设置扩容条件为int的最大值,这种情况还是比较少的
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//否则就让oldCap左移1位实现数值大小翻倍赋值给newCap 如果newCap小于数组的可设置的最大程度且原数组的oldCap >= 了默认的16 也让新的扩容阈值等于原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/*下面两种情况oldCap=0也就是散列表没有初始化,为null,第一次添加元素需要初始化 能进入此逻辑说明 是通过有参构造的,因为这三个构造器都能给threshold赋值
1.new HashMap( initCap, loadFactor) ;
2. new HashMap(initCap)
3. new HashMap(map); 并且这个map有数据
直接让数组的大小等于threshold,因为前面计算的threshold一定是2的次方*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//这种情况就是无参构造 也就是直接 new HashMap()。设置散列表大小为16,扩容阈值为0.75*16=12
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//newThr为零时,通过newCap和loadFactor计算出一个newThr
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//上面的事情就是计算新数组的大小以及扩容的阈值
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创造出一个更长更大的数组
table = newTab; //赋值给table
//如果旧表不为空 表明扩容之前有数据
if (oldTab != null) {
//就进入循环,一个桶一个桶的处理
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;//当前节点
//如果当前桶的头结点不为空
if ((e = oldTab[j]) != null) {
//将该桶置空,方便回收,因为该节点的值已经赋值给e了,所以就直接操作e
oldTab[j] = null;
//满足此条件 说明当前位置只有一个数据,直接把该节点数据放到新表对应的位置
if (e.next == null)
//index = (length - 1) & hash 这个就是确定桶的下标的
newTab[e.hash & (newCap - 1)] = e;
//如果是树形结构,就树化操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//否则就进行链表操作,高低位
else {
// 低位链表:存放在扩容之后的数组下标的位置,与当前数组下标位置一致的元素
// 高位链表:存放在扩容之后的数组下标的位置 为当前数组下标位置+ 扩容之前数组长度的元素
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 循环操作,将链表拆分为高低链
do {
next = e.next;
//如果当前hash值 & 旧数组大小 == 0 就放在低位链
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);
//将低位链的最后一个指向null,并且把它放在与原数组index一样的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将高位链的最后一个指向null,并且把它放在新数组index下标为(原数组index+原数组长度)的位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
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) {
// tab:引用当前hashmap的table first:桶位中的头元素
// n:table的长度 e:是临时Node元素 k:是key的临时变量
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1.如果哈希表为空,或key对应的桶为空,返回null,否则进入到if中
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2.这个桶的头元素就是想要找的,直接返回这个头元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 说明当前桶位不止一个元素,可能是链表,也可能是红黑树
if ((e = first.next) != null) {
// 3.树化了
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 4.链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
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:引用当前hashmap的table p:当前的node元素
// n:当前的散列表数组长度 index:表示寻址结果的下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 1.如果数组table为空或key映射到的桶为空,返回null。否则进入if条件进行查找
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
// node:查找到的结果 e:当前Node的下一个元素
Node<K,V> node = null, e; K k; V v;
// 2.如果桶位的头元素就是我们要找的
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 3.如果是树形结构
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 4.如果是链表
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; //找到了就赋值给node 然后跳出循环
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果node不为null,说明按照key查找到想要删除的数据了
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;
}
当我们有一个需求,比如40亿个数,然后内存只有1G的大小,统计数中出现最多的数?
我们最简单的思路就是使用HashMap,然后遍历,每新出现一个就加到map中,然后初始化为1,遇到一样的就进行++操作。但是表中,key和value都是整型,4个字节,所以一个数就会需要8个字节来统计,如果出现最坏的情况,每个数都不一样,则需要8*40亿字节大小=32G > 1G,这个时候内存就会出现不够用的情况。所以换个思路。
我们可以设计一个hash函数,再创建100个小文件。然后让当前的数据进行hash,得到hash值后,然后再模上100,则相同的数或者成100倍数的数都可以均匀分到100个小文件。然后再对这100个小文件分别使用HashMap统计每个小文件的最大数,统计完一个小文件,内存释放掉,然后统计另外一个小文件,然后再比较获得最大的数
增加、删除、查找时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:
1)拿到 key 的 hashCode 值;
2)将 hashCode 的高位参与运算,重新计算 hash 值;
3)将计算出来的 hash 值与 “table.length - 1” 进行 & 运算。
HashMap 的默认初始容量(capacity)是 16,capacity 必须为 2 的幂次方;默认负载因子(load factor)是 0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。
HashMap 在触发扩容后,阈值会变为原来的 2 倍,并且会对所有节点进行重 hash 分布,重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。导致 HashMap 扩容后,同一个索引位置的节点重 hash 最多分布在两个位置的根本原因是:1)table的长度始终为 2 的 n 次方;2)索引位置的计算方法为 “(table.length - 1) & hash”。
HashMap 是非线程安全的,在并发场景下使用 ConcurrentHashMap 来代替,该源码可以看我写的JUC笔记
与 Hashtable 的比较:
继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。
LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。
void afterNodeAccess(Node<K,V> p) {
}
void afterNodeInsertion(boolean evict) {
}
当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
void afterNodeAccess(Node<K,V> e) {
// move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。
evict 只有在构建 Map 的时候才为 false,在这里为 true。
void afterNodeInsertion(boolean evict) {
// possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 3;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.get(1);
cache.put(4, "d");
System.out.println(cache.keySet());
}
[3, 1, 4]