文章目录
- HashMap
-
- 几个知识点
- 构造器
- 主干
- 字段
- put
- hash
- putVal
- resize
- 三个前提条件
- get
- getNode
- 面试题
-
- 1、HashMap 的底层数据结构是怎样的?
- 2、HashMap的key或value可以为空吗?
- 3、为什么要改成“数组+链表+红黑树”?
- 4、那在什么时候用链表?什么时候用红黑树?
- 5、为什么链表转红黑树的阈值是8?
- 6、那为什么转回链表节点是用的6而不是复用8?
- 7、那 HashMap 有哪些重要属性?分别用于做什么的?
- 8、threshold 除了用于存放扩容阈值还有其他作用吗?
- 9、HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?
- 10、这个 N次方 是怎么算的?
- 11、解释一下这段代码?
- 12、HashMap 的容量必须是 2 的 N 次方,这是为什么?
- 13、 HashMap 的默认初始容量是 16,为什么是16而不是其他的?
- 14、负载因子默认初始值是多少?为什么不是其他的?
- 15、HashMap 的插入流程是怎么样的?
- 16、计算 key 的 hash 值,是怎么设计的?
- 17、为什么要将 hashCode 的高16位参与运算?
- 18、扩容(resize)流程介绍下?
- 19、红黑树和链表都是通过 e.hash & oldCap == 0 来定位在新表的索引位置,这是为什么?
- 20、HashMap 是线程安全的吗?
- 21、介绍一下死循环问题?
- 22、总结下 JDK 1.8 主要进行了哪些优化?
- 23、除了 HashMap,还用过哪些 Map,在使用时怎么选择?
- ConcurrentHashMap
-
- 几个知识点
- put
- putVal
- get
- 面试题
-
- 1、ConcurrentHashMap默认初始容量是多少?
- 2、ConCurrentHashmap 的key,value是否可以为null。
- 3、ConCurrentHashmap 每次扩容是原来容量的几倍
- 4、ConCurrentHashmap的数据结构是怎么样的?(后面会具体分析它的put方法)
- 5、存储在ConCurrentHashmap中每个节点是什么样的,有哪些变量
- 6、ConCurrentHashmap的 put 过程是怎样的?
- 7、Java1.8中ConCurrentHashmap节点是尾插还是头插?
- 8、Java1.8中,ConCurrentHashmap什么情况下链表才会转换成红黑树进行存储?
- 9、Java1.8中,ConCurrentHashmap的get过程是怎样的?
- 10、Java1.8中,ConCurrentHashmap是如何计算它的size大小的?
- 11、ConcurrentHashMap有哪些构造函数?
- 12、ConcurrentHashMap构造函数中的并发级别有什么用?
- 13、如何理解分段锁?
- 14、什么时候扩容?
- 15、什么时候链表转为红黑树?
- 16、为什么不用ReentrantLock而用synchronized ?
- 17、ConcurrentHashMap使用什么技术来保证线程安全?
- 18、ConcurrentHashMap的 get方法是否要加锁,为什么?
- 19、ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?
- 20、ConcurrentHashMap 1.7和 1.8 的区别
HashMap
几个知识点
- 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。
- 它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。
- 这个映射函数叫做散列函数,存放记录的数组叫做散列表。
- JDK1.7:HashMap采取的是数组+链表的形式存储数据。
- JDK1.8:引入了红黑树数据结构,利用红黑树快速增删改查的特点来优化了HashMap的性能。
为什么引入红黑树?
因为HashMap存在一个问题,即使负载因子和Hash算法设计的再合理,也无法避免出现在链表上拉链过长的问题,如果极端情况下出现严重的Hash冲突,会严重影响HashMap的存取性能。
构造器
构造一个具有指定初始容量和负载因子的空HashMap 。
public HashMap(int initialCapacity, float loadFactor) {
//判断容量是否合法以及修正容量
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 判断负载因子是否合法,如果小于等于0或者isNaN,loadFactor!=loadFactor,则抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//赋值loadFactor
this.loadFactor = loadFactor;
// 通过位运算将threshold设值为最接近initialCapacity的一个2的幂次方(这里非常重要)
this.threshold = tableSizeFor(initialCapacity);
}
构造一个具有指定初始容量和默认加载因子 (0.75) 的空HashMap 。
public HashMap(int initialCapacity)
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空HashMap 。
public HashMap()
构造一个与指定Map具有相同映射的新HashMap 。 HashMap是使用默认加载因子 (0.75) 和足以容纳指定Map中的映射的初始容量创建的。
public HashMap(Map<? extends K, ? extends V> m)
主干
该表在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。 (我们还在某些操作中允许长度为零,以允许当前不需要的引导机制。)
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
字段
// 默认的初始容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量,1左移30位
static final int MAXIMUM_CAPACITY = 1 << 30;
// 构造函数中未指定时使用的默认扩容因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树的链表长度
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转红黑树的数组长度
static final int MIN_TREEIFY_CAPACITY = 64;
// 实际存储K-V键值对的个数
transient int size;
// 记录HashMap被改动的次数,由于HashMap非线程安全,modCount可用于FailFast机制
transient int modCount;
// 扩容阈值,默认16*0.75=12,当填充到13个元素时,扩容后将会变为32,
int threshold;
// 负载因子 loadFactor=capacity*threshold,HashMap扩容需要参考loadFactor的值
final float loadFactor;
put
将指定的值与此映射中的指定键相关联。如果映射先前包含键的映射,则替换旧值。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0tIKEds-1647496805497)(HashMap和ConcurrentHashMap.assets/image-20220317093936378.png)]
hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(h = key.hashCode()) ^ (h >>> 16)
hash算法的步骤:
- 第一步:通过key.hashCode()获取key的hashcode;
- 第二步:通过(h = key.hashCode()) ^ (h >>> 16)进行高16位的位运算;
- 第三步:通过(n - 1) & hash对计算的hash值取模运算,得到节点插入的数组所在位置。
putVal
hash – 键的散列
key — 钥匙
value – 要放置的值
onlyIfAbsent – 如果为真,则不更改现有值
evict – 如果为 false,则表处于创建模式。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//判断键值对数组table[i]是否为空/null,是则执行resize()来创建
n = (tab = resize()).length;
// (n - 1) & hash计算插入节点在数组中的索引index,如果tab[i] == null则直接加入tab[i]
if ((p = tab[i = (n - 1) & hash]) == null)
// 通过(n - 1) & hash对计算的hash值取模运算,得到节点插入的数组所在位置
tab[i] = newNode(hash, key, value, null);
// tab[i] != null的处理方法:
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 判断tab[i]的第一个元素(p)与插入元素key的hashcode、equals是否相等
// 相等则覆盖
e = p;
else if (p instanceof TreeNode)
// 判断tab[i]是否是红黑树节点TreeNode,是则在红黑树中插入节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 则为链表
for (int binCount = 0; ; ++binCount) {
// 节点的下一个节点p.next为null,说明遍历到了链表的最后一个节点,将当前遍历到的最后一个节点的next指向新插入的节点e
if ((e = p.next) == null) {
// 先插入链表
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//遍历tab[i]判断链表是否大于8,大于8则可能转成红黑树(前提是数组需要大于64,具体要看treeifyBin()中判断数组的长度是否大于64),满足则在红黑树中插入节点;否则在链表中插入;
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//在遍历链表的过程中如果存在key的hashcode、equals相等(说明该key存在)
p = e;
}
}
// 则替换value值即可。
if (e != null) {
// 现有键的映射