Java集合框架是Java编程中非常重要的一部分,提供了各种数据结构和算法,使得开发者能够高效地组织和操作数据。
Java集合框架的设计思想主要包括以下几个方面:
ArrayList
和LinkedList
都是List
接口的实现,但它们在插入和删除操作上有不同的性能表现。Hashtable、Vector
)被设计成线程安全的,适用于多线程环境。此外,Java提供了一些在多线程环境中使用时更安全的并发集合类,如ConcurrentHashMap
。ConcurrentModificationException
,以提醒开发者在迭代过程中不要修改集合。总体来说,Java集合框架的设计思想注重通用性、性能、可扩展性和简洁性,为开发者提供了丰富而强大的工具,用于处理各种不同类型的数据。
1、构造一个具有默认初始容量(16)和默认加载因子(0.75)的空HashMap。
2、构造一个带指定初始容量和默认加载因子(0.75)的空HashMap。
3、构造一个带指定初始容量和加载因子的空HashMap。
4、构造一个映射关系与指定Map相同的新HashMap。
5、扩展LRU实现方式与思路。
在实现一个基于LRU策略
的缓存时,通常会使用一个数据结构来存储缓存中的数据,并且需要记录数据的访问顺序。常见的数据结构是双向链表和哈希表的结合。
基于双向链表和哈希表的实现方式:
头部表示最近访问的数据
,尾部表示最久未被访问的数据
。快速查找缓存中的数据
,可以将数据的键(key)映射到对应的链表节点,以实现快速的查找和插入操作。实现LRU缓存的基本思路如下:
访问缓存
中的数据时,首先在哈希表中查找该数据是否存在。存在
,则将该数据移动到链表的头部,表示最近被访问过。不存在
,需要从后端的存储中加载数据,并插入到链表的头部,同时更新哈希表。缓存已满
时,需要淘汰链表尾部的数据,同时更新哈希表。通过这种方式,实现了一个基于LRU淘汰策略的缓存系统,可以确保最近最少被使用的数据会被及时地淘汰,从而保持缓存中的数据是最有用的。
由于put方法代码量偏多,故使用代码+注释
形式解读源码。
/**
* HashMap的put操作源码+注释
*/
public V put(K key, V value) {
// 先根据hash()方法,计算存放元素在table数组中的下标
return putVal(hash(key), key, value, false, true);
}
// hash方法
static final int hash(Object key) {
int h;
// 如果key为null,返回hash=0
// 如果key不为null,那么进行key的hashCode值的高低位异或运算,返回结果作为hash值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果第一次插入,table为空或者length等于0
if ((tab = table) == null || (n = tab.length) == 0)
// 调用resize方法进行初始化
n = (tab = resize()).length;
// 通过hash值计算索引位置,将该索引位置的头节点赋值给p且p为空
// 实则进行取余操作 == p=tab[hash%length]
if ((p = tab[i = (n - 1) & hash]) == null)
// 在该索引位置新增一个节点
tab[i] = newNode(hash, key, value, null);
else {
// table表该索引位置不为空,则会发生hash冲突
Node<K,V> e; K k;
// 当p节点的hash值和key跟传入的相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// p节点即为要查找的目标节点,将p节点赋值给e节点
e = p;
// 当p节点为TreeNode
else if (p instanceof TreeNode)
// 调用红黑树的putTreeVal方法查找目标节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 判断完,则p节点为普通链表节点
//遍历节点操作,使用binCount统计链表的节点数
for (int binCount = 0; ; ++binCount) {
// p的next节点为空时,代表找不到目标节点
if ((e = p.next) == null) {
// 新增一个节点并插入链表尾部
p.next = newNode(hash, key, value, null);
// 当binCount节点数超过8个
// TREEIFY_THRESHOLD - 1:由于循环是从p节点的下一个节点开始的
if (binCount >= TREEIFY_THRESHOLD - 1)
// 调用treeifyBin方法将链表节点转为红黑树节点
treeifyBin(tab, hash);
break;
}
// e节点的hash值和key值与传入的相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// e节点即为目标节点,跳出循环
break;
// 将p指向下一个节点,继续往后遍历
p = e;
}
}
// e节点不为空,代表目标节点存在
if (e != null) {
// 使用传入的value覆盖该节点的value,即保留原来元素的value
V oldValue = e.value;
// 当oldValue为空
if (!onlyIfAbsent || oldValue == null)
// 替换value
e.value = value;
// 用于LinkedHashMap
afterNodeAccess(e);
// 返回一个原有的value
return oldValue;
}
}
// 添加操作执行后,对modCount、size做加一操作
++modCount;
// 插入节点后节点数超过阈值
if (++size > threshold)
// 调用resize方法进行扩容
resize();
// 用于LinkedHashMap
afterNodeInsertion(evict);
return null;
}
/**
* 当阈值达到触发红黑树的put的源码+注释
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// TreeNode继承于LinkedHashMap.Entry
// parent节点不等于空,则查找根节点
// 即得出索引位置的头节点并不一定为红黑树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
// 将根节点赋值给p节点,开始进行查找
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 当传入的hash值小于p节点的hash值
if ((ph = p.hash) > h)
// 将dir赋值为-1,代表向p的左边查找树
dir = -1;
// 否则传入的hash值大于p节点的hash值
else if (ph < h)
// 将dir赋值为1,代表向p的右边查找树
dir = 1;
// 当传入的key值等于p节点的key值
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// 即p节点为目标节点, 返回p节点
return p;
// 当k所属的类没有实现Comparable接口 或 k和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 由于局部变量初始为false,即当第一次判断是符合条件的
if (!searched) {
TreeNode<K,V> q, ch;
// 查找过后标记为true
searched = true;
// 从p节点的左节点和右节点分别调用find方法进行查找
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
// 查找到目标节点则返回
return q;
}
// 否则使用定义的一套规则来比较k和p节点的key的大小, 用来决定向左还是向右查找
// dir<0则代表k
dir = tieBreakOrder(k, pk);
}
// xp赋值为x的父节点,中间变量,用于下面给x的父节点赋值
TreeNode<K,V> xp = p;
// dir<=0则向p左边查找,否则向p右边查找,如果为null
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// xp的next节点
Node<K,V> xpn = xp.next;
// 创建新的节点, 其中x的next节点为xpn, 即将x节点插入xp与xpn之间
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
// 调整x、xp、xpn之间的属性关系
// 如果dir <= 0
if (dir <= 0)
// 代表x节点为xp的左节点
xp.left = x;
else
// 如果dir> 0, 则代表x节点为xp的右节点
xp.right = x;
// 将xp的next节点设置为x
xp.next = x;
// 将x的parent和prev节点设置为xp
x.parent = x.prev = xp;
// 当xpn不为空
if (xpn != null)
// 将xpn的prev节点设置为x节点,与上文的x节点的next节点对应
((TreeNode<K,V>)xpn).prev = x;
// 进行红黑树的插入平衡调整
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
/**
* 使用定义的一套规则来比较k和p节点的key的大小
*/
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
// a < b为-1,a > b为1
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
/**
* 将链表转为红黑树
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 当tab为空或者tab的长度小于64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 进行扩容
resize();
// 根据hash值计算索引值,将该索引位置的节点赋值给e且e不为null
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// 从e开始遍历该索引位置的链表
do {
// 将链表节点转红黑树节点
TreeNode<K,V> p = replacementTreeNode(e, null);
// tl为空代表为第一次循环
if (tl == null)
// 将头节点赋值给hd
hd = p;
else {
// 如果不是第一次遍历
// 当前节点的prev属性设为上一个节点
p.prev = tl;
// 上一个节点的next属性设置为当前节点
tl.next = p;
}
// 将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作(p.prev = tl 和 tl.next = p)
tl = p;
} while ((e = e.next) != null);
// 将tab该索引位置赋值为新的TreeNode的头节点,如果该节点不为空
if ((tab[index] = hd) != null)
// 以头节点(hd)为根节点, 构建红黑树
hd.treeify(tab);
}
}
// 将链表节点转红黑树节点
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
* 将哈希表中的链表结构转换为树形结构,以提高查找效率
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 开始一个循环,初始化变量x为当前节点,然后在每次迭代中将x更新为next
for (TreeNode<K,V> x = this, next; x != null; x = next) {
// next赋值为x的下个节点
next = (TreeNode<K,V>)x.next;
// 将x的左右节点设置为空
x.left = x.right = null;
// 如果还没有根节点
if (root == null) {
// 根节点没有父节点,设为null
x.parent = null;
// 根节点必须为黑色,false为黑色
x.red = false;
// 在将x设置为根节点
root = x;
}
// 如果有根节点
else {
// k赋值为x的key
K k = x.key;
// h赋值为x的hash值
int h = x.hash;
// 定义一个类型未知的变量kc
Class<?> kc = null;
// 开始一个无限循环,初始化变量p为root节点,表示对树进行遍历
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 比较当前节点x的哈希值h与节点p的哈希值ph,根据比较结果给变量dir赋值
// 如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找
if ((ph = p.hash) > h)
dir = -1;
// 如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找
else if (ph < h)
dir = 1;
// x的hash值和p的hash值相等,则比较key值
// 如果k没有实现Comparable接口 或者 x节点的key和p节点的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左还是向右查找
dir = tieBreakOrder(k, pk);
// 将p节点的父节点赋值给xp,中间变量用于下面给x的父节点赋值
TreeNode<K,V> xp = p;
// dir<=0则向p左边查找,否则向p右边查找,如果为null,则代表该位置即为x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// x的父节点即为最后一次遍历的p节点
x.parent = xp;
// 如果dir <= 0, 代表x节点为父节点的左节点
if (dir <= 0)
xp.left = x;
// 如果dir > 0, 代表x节点为父节点的右节点
else
xp.right = x;
// 进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
// 如果root节点不在tab索引位置的头节点, 则将其调整为头节点
moveRootToFront(tab, root);
}
/**
* HashMap的get操作源码+注释
*/
public V get(Object key) {
Node<K,V> e;
// 调用hash(key)方法计算键key的哈希值,然后调用getNode方法获取与该键对应的节点,将结果赋给变量e
// 如果e为null,则返回null;否则返回e节点的值e.value
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 获取与该键对应的节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 对table进行校验:table不为空 && table长度大于0 && table索引位置(使用table.length - 1和hash值进行位与运算)的节点不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查first节点的hash值和key是否和入参的一样,如果一样则first即为目标节点,直接返回first节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果first不是目标节点,并且first的next节点不为空则继续遍历
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 如果是红黑树节点,则调用红黑树的查找目标节点方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 继续遍历
do {
// 执行链表节点的查找,向下遍历链表, 直至找到节点的key和入参的key相等时,返回该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 找不到符合的返回空
return null;
}
/**
* HashMap的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) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 如果table不为空 && table长度大于0 && 根据hash值计算出来的索引位置不为空, 将该位置的节点赋值给p
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果p的hash值和key都与入参的相同, 则p即为目标节点, 赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 否则将p.next赋值给e,向下遍历节点
else if ((e = p.next) != null) {
// 如果p是TreeNode则调用红黑树的方法查找节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 否则,进行普通链表节点的查找
do {
// 当节点的hash值和key与传入的相同,则该节点即为目标节点
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
// 赋值给node, 并跳出循环
node = e;
break;
}
// p节点赋值为本次结束的e,在下一次循环中,e为p的next节点
p = e;
// e指向下一个节点
} while ((e = e.next) != null);
}
}
// 如果node不为空,即根据传入key和hash值查找到目标节点,则进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是TreeNode则调用红黑树的移除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果node是该索引位置的头节点则直接将该索引位置的值赋值为node的next节点
// “node == p”只会出现在node是头节点的时候,如果node不是头节点,则node为p的next节点
else if (node == p)
tab[index] = node.next;
// 否则将node的上一个节点的next属性设置为node的next节点
// 即将node节点移除, 将node的上下节点进行关联(链表的移除)
else
p.next = node.next;
++modCount;
--size;
// 供LinkedHashMap使用
afterNodeRemoval(node);
// 返回被移除的节点
return node;
}
}
return null;
}
/**
* HashMap的resize操作源码+注释
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 旧表的容量不为0
if (oldCap > 0) {
// 判断旧表的容量是否超过最大容量值
if (oldCap >= MAXIMUM_CAPACITY) {
// 将阈值设置为Integer.MAX_VALUE,并直接返回旧表
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将newCap赋值为oldCap的2倍,如果newCap<最大容量小于最大容量值并且oldCap>=16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将新阈值设置为原来的两倍
newThr = oldThr << 1; // double threshold
}
// 如果旧表的容量的阈值大于0, 是因为初始容量被放入阈值,则将新表的容量设置为旧表的阈值
else if (oldThr > 0)
newCap = oldThr;
else {
// 旧表的容量为0, 旧表的阈值为0,这种情况是没有传初始容量的new方法创建的空表,将阈值和容量设置为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新表的阈值为空, 则通过新的容量*负载因子获得阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将当前阈值设置为刚计算出来的新的阈值,定义新表,容量为刚计算出来的新容量,将table设置为新定义的表
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果旧表不为空,则需遍历所有节点,将节点赋值给新表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 将索引值为j的旧表头节点赋值给e
if ((e = oldTab[j]) != null) {
// 将旧表的节点设置为空, 以便垃圾收集器回收空间
oldTab[j] = null;
// 如果e.next为空, 则代表旧表的该位置只有1个节点,计算新表的索引位置, 直接将该节点放在该位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果是红黑树节点,则进行红黑树的重hash分布(跟链表的hash分布基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 如果是普通的链表节点,则进行普通的重hash分布
// 存储索引位置为:“原索引位置”的节点
Node<K,V> loHead = null, loTail = null;
// 存储索引位置为:“原索引位置+oldCap”的节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 如果e的hash值与旧表的容量进行与运算为0,则扩容后的索引位置跟旧表的索引位置一样
if ((e.hash & oldCap) == 0) {
// 如果loTail为空
if (loTail == null)
// 将loHead赋值为第一个节点
loHead = e;
else
// 否则将节点添加在loTail后面
loTail.next = e;
// 并将loTail赋值为新增的节点
loTail = e;
}
else {
// 如果hiTail为空, 代表该节点为第一个节点
if (hiTail == null)
// 将hiHead赋值为第一个节点
hiHead = e;
else
// 否则将节点添加在hiTail后面
hiTail.next = e;
// 并将hiTail赋值为新增的节点
hiTail = e;
}
} while ((e = next) != null);
// 如果loTail不为空(说明旧表的数据有分布到新表上“原索引位置”的节点),则将最后一个节点的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 如果hiTail不为空(说明旧表的数据有分布到新表上“原索引+oldCap位置”的节点),则将最后一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新表
return newTab;
}
合理使用HashMap的建议:
计算结果
或者频繁访问的数据
存储在HashMap中,以提高数据的访问速度。键值对形式的配置参数
。频繁计算
的场景下,可以使用HashMap来缓存计算结果,避免重复计算。查找、插入和删除
操作,适合于需要频繁进行这些操作的场景。最佳实践:
初始化容量: 在创建HashMap时,尽量提供初始容量,以减少扩容操作的频率。这可以通过构造函数中的参数来完成,如 HashMap(int initialCapacity)
。
Map<String, Integer> map = new HashMap<>(16); // 初始化容量为16
负载因子: 负载因子是指哈希表在自动扩容之前可以达到多满的一个度量。默认负载因子是0.75,你可以根据你的需求调整。
Map<String, Integer> map = new HashMap<>(16, 0.8f); // 指定负载因子为0.8
选择好的哈希函数: 如果你使用自定义对象作为键,确保实现了合适的hashCode()
和equals()
方法。
线程安全性: 如果在多线程环境下使用HashMap,考虑使用ConcurrentHashMap
,它提供了更好的并发性能。
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
遍历方式: 使用entrySet()
来遍历HashMap,而不是分别使用keySet()
和values()
。这可以避免多次计算哈希码。
for (Map.Entry<String, Integer> entry : map.entrySet()) {
// 处理每个键值对
}
注意事项:
空指针检查: 在使用get()
方法获取值之前,最好先检查键是否存在,以避免空指针异常。
if (map.containsKey(key)) {
// 避免空指针异常
Integer value = map.get(key);
// 处理值
}
键和值的类型: 注意HashMap的键和值的类型,确保它们符合你的预期。使用泛型
可以在编译时
提供类型检查。
扩容代价: HashMap在达到一定负载因子时会自动扩容,这可能导致性能损失。在性能敏感的场景中,可以考虑手动调整容量
以减少扩容的发生。
不要过度使用HashMap: 在某些情况下,可能有更适合的数据结构,如LinkedHashMap、TreeMap等,取决于你的需求。
了解并发限制: 如果在并发环境下使用HashMap,要注意可能出现的并发限制,确保你的代码是线程安全的。
HashMap的全面总结:
HashMap
是Java集合框架中的一部分,实现了Map
接口,用于存储键值对。put(key, value)
方法用于插入键值对。get(key)
方法用于根据键获取值。remove(key)
方法用于根据键删除键值对。0.75
,表示当HashMap中的元素个数达到容量乘以负载因子时,进行扩容操作。O(1)
,在理想情况下。ConcurrentHashMap
。equals
和hashCode
方法。盈若安好,便是晴天