参考文献

理论

HashMap最早是在JDK1.2中出现,直到JDK1.7一直没太大变化,但是到了JDK1.8突然进行了一个很大的改动.其中一个显著的改动就是:

  • 之前JDK1.7的存储结构是数组+链表

  • 到了JDK1.8变成了数组+链表+红黑树.

另外,HashMap是非线程安全的,也就是说多线程同时对HashMap中的某个元素进行增删改操作时,是不能保证数据的一致性的.

image-20210417194922322

在JDK1.7中,首先是把元素放在一个个数组里面,后来存放的数据元素越来越多,于是就出现了链表,对数组中的每个元素,都可以有一条链表来存储元素.这就是有名的"拉链式"存储方法

后来存储的元素越来越多,链表也越来越长,查找一个元素的时候效率不仅没有提高(链表不适合查找,适合增删),反而下降了不少,于是在JDK1.8中对这条链表进行了改进–使用红黑树. 将链表结构编程红黑树,原来JDK1.7的优点是增删效率提高,在JDK1.8中不仅增删的效率提高了,查找的效率也提升了.

核心是基于哈希值的桶和链表

  • 一般用数组实现桶
  • 发生哈希碰撞时,用链表来链接发生碰撞的元素
  • O(1)的平均查找、插入、删除时间
  • 致命缺点是哈希值的碰撞(collision)
    • 哈希碰撞:元素通过哈希函数计算后,被映射到同一个桶中.例如上图中的桶1, 5中的元素就发生了哈希碰撞

image-20210417195442294

  • 链表变成红黑树的条件:只有链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树.

为什么选择红黑树?

  • 红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常高的,查找效率会从链表的O(n)降低为O(logn);
  • 引入红黑树就是为了查找数据快O(log n),解决链表查询深度的问题
  • 但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当桶中元素大于8并且桶的个数大于64的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢

红黑树性质

  • 每个节点非红即黑
  • 根节点(root)是黑的
  • 不能有两个红色的节点连在一起(黑色可以)
  • 每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的
  • 如果一个节点是红的,那么它的两儿子都是黑的
  • 对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点
  • 每条路径都包含相同的黑节点

为什么不一下子把整个链表转变为红黑树?

  • 构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能上看,数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高;
  • HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的,因此,也就是链表长度比较长的时候转变为红黑树才会效率显著;
  • 链表变为红黑树的条件:元素个数大于8同时桶的个数大于64
    • 当某个桶中的元素个数大于8时,会调用treeifyBin()方法,但并不一定就会变为红黑树
    • 当哈希表中桶的个数大于64时,才会真正进行让其转化为红黑树

HashMap初始容量

  • initialCapacity初始容量

    官方要求我们要输入一个2的N次幂的值,比如说2、4、8、16等等这些,但是我们忽然一个不小心,输入了一个20怎么办?没关系,虚拟机会根据你输入的值,找一个离20最近的2的N次幂的值,比如说16离他最近,就取16为初始容量.

HashMap负载因子

  • loadFactor负载因子

    负载因子,默认值是0.75.负载因子表示一个散列表的空间的使用程度,有这样一个公式:initailCapacity*loadFactor=HashMap的容量. 所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低.反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高.

    泊淞分布

    当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的.当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个是几乎不可能的.

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;

/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
 /**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity 初始容量 要求要输入一个2的N次幂的值
* @param loadFactor the load factor 负载因子,默认值是0.75.
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
// 如果传入的初始化容量小于0,则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果传入的容量大于最大容量,就初始化为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果负载因子小于0,或者是非法的浮点数,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 负载因子表示一个散列表的空间的使用程度 initialCapacity*loadFactor=HashMap的容量
// 所以负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低.
// 反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成浪费,但是此时索引效率高.
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
// 经过下面的 或 和位移 运算, n最终各位都是1.
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 判断n是否越界,返回 2的n次方作为 table(哈希桶)的阈值
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Implements Map.putAll and Map constructor.
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// 获取该map的实际长度
int s = m.size();
if (s > 0) {
// 判断table是否初始化
if (table == null) { // pre-size
/**
* 求出需要的容量,因为实际使用的长度=容量*0.75得来的,+1是因为小数相除,基本都不会是整数,容量大小不能为小数的,后面转换为int,多余的小数就要被丢掉,所以+1,
* 例如,map实际长度22,22/0.75=29.3,所需要的容量肯定为30,有人会问如果刚刚好除得整数呢,除得整数的话,容量大小多1也没什么影响
**/
float ft = ((float)s / loadFactor) + 1.0F;
//判断该容量大小是否超出上限.
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果table已经初始化,则进行扩容操作,resize()就是扩容.
else if (s > threshold)
resize();
//遍历,把map中的数据转到hashMap中.
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}

存储元素(put)

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
map.put("k1", "v1");
}
}
第一步:调用put方法传入键值对
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
// 1.先根据键计算Hash
return putVal(hash(key), key, value, false, true);
}
第二步:使用hash算法计算hash值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
// hash值其实就是通过hashcode与16异或计算的
// 通过异或运算能够是的计算出来的hash比较均匀,不容易出现冲突
// 在数据结构中,我们处理hash冲突常使用的方法有:开发定址法、再哈希法、链地址法、建立公共溢出区.而hashMap中处理hash冲突的方法就是链地址法.
// 这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行.链地址法适用于经常进行插入和删除的情况.
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • h = key.hashCode()表示获取 key 的哈希值,然后将它右移 16 位,也就是h >>> 16;
  • 右移16位的目的是:是为了让高位参与到新的哈希值的计算中,从而减小哈希冲突的概率.
    • 因为在 HashMap 中,哈希值相同的 key 会放在同一个桶中,如果哈希冲突太多,桶中的链表就会过长,影响了 HashMap 的性能.
    • 通过将哈希值的高位与低位进行异或操作,可以保证高位的信息参与到新的哈希值的计算中,从而减小哈希冲突的概率,提高 HashMap 的性能.这也是一种均衡哈希算法的实现方式.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Implements Map.put and related methods.
*
* @param hash hash for key 调用了hash方法计算得到的hash值
* @param key the key 传入的key的值
* @param value the value to put 传入的value的值
* @param onlyIfAbsent if true, don't change existing value true表示: 当键相同时,不修改已存在的值
* @param evict if false, the table is in creation mode. false表示: 数组就处于创建模式,一般为true
* @return previous value, or null if none
*/
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;
// 1. 延迟初始化逻辑,第一次调用putVal会初始化hashMap对象中最好内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
// 如果table尚未初始化,则此处进行初始化数组,并赋值初始容量,重新计算阈值
n = (tab = resize()).length;

// 2.1 在这里要判断插入的位置是否是冲突的 i表示在数组中插入的位置,计算的方式为(n-1)&hash,
if ((p = tab[i = (n - 1) & hash]) == null)
// 若不冲突,就直接newNode,插入到数组中即可
tab[i] = newNode(hash, key, value, null);

else {
// 2.2 如果通过hash找到的位置有数据,发生碰撞
// e: 不为null时,找到了一个与当前要插入的key-value一致的key的元素
// k: 表示临时元素的key
Node<K,V> e; K k;
// 这里判断table[i]中的元素是否与插入的key一样,若相同那么就使用插入的值p替换掉就的值e;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 2.2.1表示桶位中的该元素与当前要插入的元素的key完全一致,表示后续需要进行替换操作
e = p;

// 判断插入的数据结构是红黑树还是链表
else if (p instanceof TreeNode)
// 2.2.2 若是红黑树,那么直接调用putTreeVal(),将节点加到红黑树中
// 如果该节点已经存在,则返回该节点(不为null),该值很重要,用来判断put操作是否成功,如果添加成功返回null
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

// 若是链表
else {
// 2.2.3 链表情况,表示链表的头元素与当前要插入的key不一致
// 需要遍历链表找到插入的位置
for (int binCount = 0; ; ++binCount) {
// 2.2.3.1 如果找到尾部,则表明添加的key-value没有重复,在尾部进行添加
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 若此时的数据结构是链表,插入的时候会判断当前链表的长度是否大于等于阀值TREEIFY_THRESHOLD - 1
// 8表示前面已经有了至少7个元素(从0开始),需要进行树化操作
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 插入之后大于8了,就要先调整为红黑树,在插入
treeifyBin(tab, hash);
break;
}

// 2.2.3.2 条件为 true,表示找到了相同key的Node元素,需要进行替换操作,终止遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 移动指针向后遍历
p = e;
}
}

// 2.3 e!=null,表示找到了与当前需要插入元素key一致的数据,表示需要替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}

// modCount: 表示散列表结构被修改的次数,替换Node元素的value不计数
++modCount;
// 插入成功之后,还要判断一下实际存在的键的数量size是否大于阀值threshold,若大于则需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
  • HashMap 在创建对象时,并不会立即创建 table 数组,而是在第一次添加元素时进行延迟初始化.这样做的目的是为了节省内存空间,避免在创建对象时就分配大量的内存空间,浪费内存资源.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;

// 如果当前哈希表中桶的数目,小于最小树化容量,就调用resize()方法进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();

// 当桶中元素个数大于8,且桶的个数大于64时,进行树化
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}

img

扩容(resize)

  • 扩容是为了避免哈希冲突过多,导致链表长度过长,从而影响了 HashMap 的查询和操作效率.

  • 主要操作:

    • 第一步: 先计算扩容后新table数组的大小newCap和扩容后新的扩容阈值newThr

      • 第一种情况: oldCap > 0 老数组容量oldCap(扩容前的数值大小)大于0

        • 继而判断老数组容量是否大于等于最大容量阈值,若超过了则将扩容阈值设置为int的最大值,返回老数组.
        • 若不满足上面的条件,即老数组容量小于最大容量阈值,则将新的数组容量设置为老数组的2倍
          • 若新数组容量小于最大容量阈值且老数组容量大于等于默认初始化容量(DEFAULT_INITIAL_CAPACITY,值为16),则将新扩容阈值设置为老扩容阈值的2倍.
      • 第二种情况: oldCap == 0 && oldThr > 0

        • 以下三种情况满足上面的条件

          1
          2
          3
          // 1. new HashMap(initCap,loadFactor)
          // 2. new HashMap(initCap)
          // 3. new HashMap(map),且这个map有数据
        • 这种情况则将老扩容阈值oldThr,即threshold的值赋值给newCap

      • 第三中情况: oldCap == 0 && oldThr == 0

        • 以下情况满足上面的条件

          1
          2
          // 调用无参的构造函数oldCap,oldThr都未进行赋值
          // 1. new HashMap();
        • 这种情况将默认初始化容量(DEFAULT_INITIAL_CAPACITY,值为16)赋值给新数组容量newCap,继而再根据负载因子计算新的扩容阈值,计算公式为(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)

    • 第二步:

      • 将计算完毕后的newThr赋值给属性变量threshold,
      • 开辟新数组内存,将新数组newTab赋值给属性变量table
      • 若老数组不为空,则将老数组的元素转移到新数组上.
        • 遍历老数组,获取老数组上节点,判断当前节点是什么结构?
          • 若当前节点是单个节点,则根据当前节点的hash值计算出当前节点应该在新数组上的位置,最后插入当前节点;
          • 若当前节点是红黑树,则对红黑树进行拆分;
          • 若当前节点是链表,则采用两个链表(高低位链表)来重新组织节点,根据(e.hash & oldCap) == 0来判断将节点加入高位链表还是低位链表
            • 若满足条件,则将节点加入到低位链表中
            • 若不满足条件,即(e.hash & oldCap) == 1,则将节点加入到高位链表中
            • 最后将高低位链表装载到新数组上,装载规则为:
              • 低位链表依然装载到原来的槽位上,即若链表在老数组下标为j的位置上,低位链表就在新数组下标j
              • 高位链表则需要在原来的槽位上进行偏移老数组的长度,即若链表在老数组下标为j的位置上,高位链表就在新数组下标j+oldCap
      • 最终返回处理完毕的新数组

HashMap扩容就是先计算,新的hash表容量和新的容量阀值,然后初始化新的hash表,将旧的键值对重新映射到新的hash表里面.若在就的hash表里面涉及到红黑树,那么在映射到新的hash表中还涉及红黑树的拆分.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
// oldTab: 表示引用扩容前的散列表table
Node<K,V>[] oldTab = table;
// oldCap: 扩容前的散列表table的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// oldThr: 扩容前的散列表的扩容阈值,即触发本次扩容的阈值
int oldThr = threshold;
// newCap: 扩容之后table数组的大小
// newThr: 扩容后,下次再触发扩容的条件
int newCap, newThr = 0;
if (oldCap > 0) {
// oldCap > 0 表示,hashmap之前已经初始化过了,此次是一次正常扩容
// 若扩容之前table数组大小已经达到最大容量阈值,则不扩容,将扩容阈值设置为int的最大值,返回原来的table数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// newCap: 扩容后的容量,是扩容之前的容量的2倍
// 若扩容后的容量newCap小于最大容量阈值 且 扩容前的容量大于等于初始化时的容量(16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将扩容阈值也变为原来的2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// oldCap == 0 && oldThr > 0
// 1. new HashMap(initCap,loadFactor)
// 2. new HashMap(initCap)
// 3. new HashMap(map),且这个map有数据
/*
* 初始化时,将 threshold 的值赋值给 newCap,
* HashMap 使用 threshold 变量暂时保存 initialCapacity 参数的值
*/
newCap = oldThr;
else {
// zero initial threshold signifies using defaults
// oldCap == 0 && oldThr == 0
// new HashMap()
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// oldCap == 0 && oldThr > 0 || oldCap < DEFAULT_INITIAL_CAPACITY ==> newThr == 0
// 没有初始化,初始化一个新的数组容量和新的阀值
float ft = (float)newCap * loadFactor;
// 新的扩容阈值等于容量 * 加载因子
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 把上面各种情况分析出的临界值,在此处真正进行改变,也就是容量和临界值都改变了.
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) {
// e: 临时变量,表示当前遍历到的节点
Node<K,V> e;
// (e = oldTab[j]) != null ==> 表示老数组的中当期遍历位置j上有数据,则需要将其迁移到新数组上
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 区别当前Node时什么结构? 是单个节点? 是红黑树节点? 还是链表节点
if (e.next == null)
// 若当前节点没有后续节点,则表示单个节点,则从根据hash值计算出当前节点应该在新数组上的位置
// 把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j
newTab[e.hash & (newCap - 1)] = e;

else if (e instanceof TreeNode)
// 是红黑树节点
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

else { // preserve order
// 是链表节点
// 低位链表: 存放在扩容之后的数组的下标位置,与当前数组的下标位置一致
Node<K,V> loHead = null, loTail = null;
// 高位链表: 存放在扩容之后的数组的下标位置为 当前数组的下标位置 + 扩容之前数组的长度
Node<K,V> hiHead = null, hiTail = null;
// 遍历指针next
Node<K,V> next;
do {
next = e.next;
// 扩容后长度为原hash表的2倍,于是把hash表分为两半,分为低位和高位,把原链表的键值对, 一半放在低位,一半放在高位
// 若oldCap = 16,二进制为10000,第5位为1,
// e.hash & oldCap 是否等于0就取决于e.hash第5位是0还是1,
// 这就相当于有50%的概率放在新hash表低位,50%的概率放在新hash表高位
if ((e.hash & oldCap) == 0) {
// 若(e.hash & oldCap) == 0,存放于低位链表中
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 若(e.hash & oldCap) == 1,存放于高位链表中
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);

if (loTail != null) {
// 低位链表存放于原来的散列表的位置上
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// 高位链表存于原索引加上原hash桶长度偏移量
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

移除元素(remove)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key) {
// 临时变量
Node<K,V> e;
// 调用removeNode(hash(key), key, null, false, true)进行删除,
// 第三个value为null,表示,把key的节点直接都删除了,不需要用到值,如果设为值,则还需要去进行查找操作
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* Implements Map.remove and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to match if matchValue, else ignored
* @param matchValue if true only remove if value is equal 为true的话,则表示删除它key对应的value,不删除key
* @param movable if false do not move other nodes while removing 为false,则表示删除后,不移动节点
* @return the node, or null if none
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// tab: 引用当前hashMap的散列表
// p: 当前Node元素
// n: 散列表数组长度
// index: 表示寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 散列表不为空 && 散列表长度不为空 && 当前hash在散列表桶位上的元素不为空
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;
// 第一步: 先查询
// 第一种情况: 若当前桶位上的元素p的hash,key与要删除的元素一致,则表示找到了要删除的元素,将p赋值给node方便后续移除元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 第二种情况: 若当前桶位上的元素p不是要找的元素,需要进而判断Node节点结构是红黑树还是链表
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 判断当前桶位是否升级为红黑树
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 当前桶位为链表,则遍历链表查找与要找的元素,若找到了则赋值给node方便后续移除元素
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
// 注意,如果进入了链表中的遍历,那么此处的p不再是数组下标的节点,而是要删除结点的上一个结点
p = e;
} while ((e = e.next) != null);
}
}
// 第二步: 删除元素
// 找到要删除的节点后,判断!matchValue,我们正常的remove删除,!matchValue都为true
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
// 第三种情况: 节点结构为链表,则将要删除节点的下一个元素赋值给删除节点的前一个节点的next
p.next = node.next;
++modCount;
// 将散列表容量减一
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}

获取元素(get)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
// tab: 引用当前hashMap的散列表
// first: 桶位中的头元素
// e: 临时Node元素
// n: table数组长度
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 散列表不为空 && 数组长度不为空 && key对应的桶位上的元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 第一种情况: 如果当前第一个元素就命中(即当前元素的hash,当前元素的key与要传入的hash和key一致),直接返回第一个元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 第二种情况: 要么Node结构是红黑树要么是链表
if ((e = first.next) != null) {
// 判断是否是红黑树结构
if (first instanceof TreeNode)
// 去红黑树中找,然后返回
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 若Node接口是链表,则进行遍历链表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 找不到,表示不存在该节点
return null;
}

快速失败(fail—fast)

  • HashMap 遍历使用的是一种快速失败机制,它是 Java 非安全集合中的一种普遍机制,这种机制可以让集合在遍历时,如果有线程对集合进行了修改、删除、增加操作,会触发并发修改异常。

  • 它的实现机制是在遍历前保存一份 modCount ,在每次获取下一个要遍历的元素时会对比当前的 modCount 和保存的 modCount 是否相等。

  • 快速失败也可以看作是一种安全机制,这样在多线程操作不安全的集合时,由于快速失败的机制,会抛出异常。

LinkedHashMap,HashMap,TreeMap的区别

img

(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的. HashMap最多只允许一条记录的键为null,允许多条记录的值为null.HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致.如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁.Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* Maps the specified <code>key</code> to the specified
* <code>value</code> in this hashtable. Neither the key nor the
* value can be <code>null</code>. <p>
*
* The value can be retrieved by calling the <code>get</code> method
* with a key that is equal to the original key.
*
* @param key the hashtable key
* @param value the value
* @return the previous value of the specified key in this hashtable,
* or <code>null</code> if it did not have one
* @exception NullPointerException if the key or value is
* <code>null</code>
* @see Object#equals(Object)
* @see #get(Object)
*/
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}

addEntry(hash, key, value, index);
return null;
}

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序.

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的.如果使用排序的映射,建议使用TreeMap.在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常.

实现方式:

  • LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的;
  • TreeMap基于SortedMap实现,

元素顺序性:

  • HashMap无序;LinkedHashMap有序,TreeMap有序(主要用于Map的排序,能够把它保存的记录根据键排序.默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的)

线程安全性:

  • LinkedHashMap,HashMap,TreeMap都是线程不安全的;