【引言】针对线性结构的两类主要集合前面已经做了解读,这一章里面,我们来阅读阅读另外一种key-value模式的数据结构,日常编码中我们最常用的想必就是HashMap了!

类的定义
1 | /** |
成员变量
1 | // 【Lin.C】:字面意思就可以理解:默认容量 |
构造方法
HashMap()
1 | // 【Lin.C】:平时我们最常用的一种构造方法,都使用默认参数 |
HashMap(int initialCapacity)
1 | /** |
HashMap(int initialCapacity, float loadFactor)
1 | /** |
HashMap(Map<? extends K, ? extends V> m)
1 | /** |
增
put
1 | /** |
删
remove
1 | /** |
clear
1 | /** |
改
replace
1 | /** |
查
get
1 | /** |
forEach
1 | /** |
isEmpty
1 | /** |
containsKey
1 | /** |
containsValue
1 | /** |
扩容
resize
1 | /** |
内部类
Node
1 | /** |
TreeNode
1 | /** |
EntrySet
1 | /** |
KeySet
1 | /** |
MUST be a power of two约定
关于MUST be a power of two这个话题,我觉得有必要谈一谈;1
2
3
4
5
6
7
8
9// JDK1.8之前呢,是这么算数组下标的(h就是hash)
static int indexFor(int h, int length) {
return h & (length-1);
}
// 但是在1.8里面indexFor这个方法就找不到了,后来发现是直接写到了方法里面了,就是下面的(n - 1) & hash
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
HashMap的初始容量和扩容都是以2的次方来进行的,那么length-1换算成二进制的话肯定所有位都为1,就比如2的3次方为8,length-1的二进制表示就是111, 而按位与计算的原则是两位同时为“1”,结果才为“1”,否则为“0”。所以h & (length-1)运算从数值上来讲其实等价于对length取模,也就是h%length。
那我们反证一下,如果不满足前提条件“HashMap的初始容量和扩容都是以2的次方来进行的”,会发生什么问题呢?
假设当前table的length是15,二进制表示为1111,那么length-1就是1110,此时有两个hash值为8和9的key需要计算索引值,计算过程如下:1
2
3
4
58的二进制表示:1000
8&(length-1)= 1000 & 1110 = 1000,索引值即为8;
9的二进制表示:1001
9&(length-1)= 1001 & 1110 = 1000,索引值也为8;
这样一来就产生了相同的索引值,也就是说两个hash值为8和9的key会定位到数组中的同一个位置上形成链表,这就产生了碰撞;而查询的时候需要遍历这个链表,这样就降低了查询的效率。
同时,我们也可以发现,当数组长度为15的时候,hash值会与length-1(1110)进行按位与,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,会造成严重的空间浪费,更糟的是这种情况下,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率。
因此可以看出,只有当数组长度为2的n次方时,不同的key计算得出的index索引相同的几率才会较小,数据在数组上分布也比较均匀,碰撞的几率也小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
此外,位运算快于十进制运算,hashmap扩容也是按位扩容,这样同时也提高了运算效率。
所以,还是一切为了效率,尽可能的达到最优。
关于put的返回值验证
1 | package com.example.demo; |
关于Hashtable
Hashtable本身现在已经基本处于被抛弃的阶段,但是了解一下之后发现也是比较暴力的解决了同步的问题,就是在方法上直接加synchronized。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public 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;
"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;
}