HashMap原理

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
import java.util.HashMap;
import java.util.Map;

public class test {
public static void main(String[] args) {
Map<Integer,String> map=new HashMap<Integer,String> (16);
System.out.println("map加入值");
map.put(1,"q");
map.put(2,"w");
map.put(3,"e");
System.out.println("判断该键是否存在:"+map.containsKey(4));
System.out.println("获取该键的值:"+map.get(1));
System.out.println("map的大小:"+map.size());
System.out.println("判断是否相等:"+"q".equals(map.get(1)));
for (String a: map.values()) {
System.out.print(a);
}
}

}

/*
map加入值
判断该键是否存在:false
获取该键的值:q
map的大小:3
判断是否相等:true
qwe
*/

HashMap的使用比较直观,可以粗略的把HashMap看做为一个数组去使用。

HashMap的定义

从类的定义来看

1
2
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

HashMap继承了抽象map类,实现了cloneable和Serializable接口。

HashMap的数据都存在一个entry数组里面,在HashMap有一个静态类去实现。

1
2
3
4
5
6
7
8
9
10
11
12
//JDK7
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
//JDK8
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

可以看到无论是JDK7还是JDK8,都定义了两种值,key和value,我们存入的数据,都是存入在这数组之中,而且还定义了一个next,next就像是链表一样,用于指向下一个值。所以table中存储的是Entry的单向链表。默认参数的HashMap结构如下图所示:

hashmap2

所以,可以把它看作为是一个数组,然后每个数组都由链表组成。

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
    public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//这里调用了this
//DEFAULT_LOAD_FACTOR是一个负载因子,默认为0.75

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//传入的大小小于0,抛出异常
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);
}

//tableSizeFor是用来纠正传入的大小的
static final int tableSizeFor(int cap) {
int n = cap - 1;
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;
}

哈希桶数组会在首次使用时初始化,默认大小是 16,并根据需要调整大小,且长度总是 2 的次幂。如果构造函数设置的初始容量不是 2 的次幂,那么使用tableSizeFor方法,来设置一个大于且靠近它的 2 的次幂的值。

影响 HashMap 性能的主要参数是:初始容量负载因子。当散列表元素数超过负载因子和当前容量的乘积时((initialCapacity * loadFactor)。),就会扩容,扩大到原来容量的两倍,并对键重新散列。

HashMap 内部的其他字段:

1
2
3
4
5
6
7
8
// 键值对的数量
transient int size;
// 记录结构修改次数,用于迭代时的快速失败
transient int modCount;
// 负载因子,默认 0.75f
final float loadFactor;
// 扩容的下一个容量值,也就是键值对个数的最大值,它等于(capacity * loadFactor)
int threshold;

HashMap的put()方法

put方式是我们在使用HashMap中经常使用的方法,所以要好好研究。

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

可以看到,我们在使用put方法的时候,实际是使用一个putVal方法。而在这个putVal方法中,会将key值先进行hash运算,得到hash码。

hash()方法

这个hash方法如下:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

如果key值为null的时候,就返回hash码为0。但如果不为null呢?

1
(h = key.hashCode()) ^ (h >>> 16);

这个会是什么意思呢?这段代码叫“扰动函数”。key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。这个求出hashcode的方法非常的复杂。

1
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-21474836482147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。

但问题是一个40亿长度的数组,内存是放不下的。你想,HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。

而这个对数组的长度取模运算的操作,正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1 )正好相当于一个“低位掩码”。

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。

这时候“扰动函数”的价值就体现出来了:

raodong

右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

此时的key值先求出hash值,再经过扰动函数之后,得出来的hash值的重复率就大大的降低了。而对数组的长度取模运算的操作,发生在putVal()方法当中。

putVal()方法

我们可以从源码上得之,使用put方法,实际上是在使用putVal方法,那我们来解析一下吧。

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
public V put(K key, V value) {
// 将 key 的 hashCode 散列
return putVal(hash(key), key, value, false, true);
}
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)
n = (tab = resize()).length;
//1.如果table 为 null,初始化哈希桶数组
//此时的n就被初始化,赋值为hashmap的大小,即n=length
//resize()方法后面会提


if ((p = tab[i = (n - 1) & hash]) == null)
//2.计算数组的下标(n - 1) & hash
//这其实就是mod取余的一种替换方式,相当于hash%(n-1)
//&是位运算,效率要高于%。至于为什么是跟n-1进行&的位运算,是因为n为2的幂次方,即一定是偶数,偶数减1,即是奇数,这样保证了(n-1)在二进制中最低位是1,而&运算结果的最低位是1还是0完全取决于hash值二进制的最低位。如果n为奇数,则n-1则为偶数,则n-1二进制的最低位横为0,则&位运算的结果最低位横为0,即横为偶数。这样table数组就只可能在偶数下标的位置存储了数据,浪费了所有奇数下标的位置,这样也更容易产生hash冲突。这也是HashMap的容量为什么总是2的平方数的原因。

tab[i] = newNode(hash, key, value, null);
// 3. 这个槽还没有插入过数据,直接插入
// 这里的p结点是根据hash值算出来对应在数组中的元素
else {
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4. else表示节点 key 存在,使用链表去连接前一个值,此后这个key返回的value值变成新值

else if (p instanceof TreeNode) // 在树中插入
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 5. 该链的链长如果大于8,则转成了红黑树进行存储

else {
// 6. esle表示该链是链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 遍历找到尾节点插入
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 链表长度大于 8 转为红黑树
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
// 遍历的过程中,遇到相同 key 则覆盖 value
}
}
if (e != null) { //现有键映射
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 判断是否允许覆盖,并且value是否为空
e.value = value;
afterNodeAccess(e);// 回调以允许LinkedHashMap后置操作
return oldValue;
}
}
++modCount; // 更改操作次数

if (++size > threshold)
// 大于临界值
// 将数组大小设置为原来的2倍,并将原先的数组中的元素放到新数组中
// 因为有链表,红黑树之类,因此还要调整他们
resize();
// 7. 超过最大容量,扩容
afterNodeInsertion(evict);
return null;
}
//JDK8 在插入链表时采用的是尾插入法,也就是顺序插入,而 JDK7 使用的是头插法,逆序插入。

PutVal方法中,会对Hash码和hashmap的长度-1做与运算,这样的运算方式比取余更为快速。从而得到的最后结果就是元素存放的地点。

putval

HashMap的扩容:resize()

JDK8的优化扩容机制

每次在空的数组中存放元素成功,就会执行++size操作,当元素存储的大小大于threshold,即大于整个数组的0.75倍时,就会触发扩容操作,把整个数组扩容成原来大小的两倍。因为使用的是2的次幂扩展,那么元素的位置要么保持不变,要么在原位置上偏移2的次幂。

看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

扩容

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

扩容2

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

扩容3

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。

我们接下来看看JDK8的resize()的源码实现吧:

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
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超过最大值,不在扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}// 否则扩大为原来的 2 倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 双倍的阈值
}
else if (oldThr > 0) // 初始容量置于阈值
// 初始化时,threshold 暂时保存 initialCapacity 参数的值
newCap = oldThr;
else { // 零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的 resize 上限
if (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) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) // 无链条
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;
Node<K,V> next;
do {
next = e.next;
// 原位置不变的子链表
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原位置偏移 oldCap 的子链表
else {
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) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}//红黑树之所以能够按照链表的逻辑拆分,是因为链表在转红黑树时,保留了原链表的链条引用,这样也方便了遍历操作。
}
}
}
return newTab;
}
链表转红黑树

链表转红黑树主要做了以下几件事:

  1. 判断桶容量是否达到树化的最低要求,否则进行扩容
  2. 将原链表转为由 TreeNode 组成的双向链表
  3. 将新链表转为红黑树
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
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果哈希桶容量小于树化的最小容量,优先进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
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); // 将链表转为红黑树
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}

HashMap 在设计时应该没有考虑后期会引入红黑树,所以没有提供 key 的比较器或要求 key 实现 Comparable 接口。为了比较两个 key 的大小,HashMap 按以下步骤处理:

  1. 如果两个 key 的 hash 值不等,则比较 hash 值大小
  2. 如果相等,若 key 实现了 Comparable 接口,使用 compareTo 方法比较
  3. 如果结果还是相等,使用自定义的 tieBreakOrder 方法比较,逻辑如下
1
2
3
4
5
6
7
8
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null || // 比较 className 的大小
(d = a.getClass().getName().compareTo(b.getClass().getName())) == 0)
// 比较由本地方法生成的 hash 值大小,仍然有可能冲突,几率太小,此时认为是小于的结果
d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
return d;
}
优化总结

JDK8 中的 HashMap 代码还是比较复杂的,优化方面主要有以下三点:

  • 优化 hash 算法只进行一次位移操作
  • 引入红黑树,在冲突比较严重的情况下,将 get 操作的时间复杂从 O(n) 降为了 O(logn)
  • 扩容时,利用 2 的次幂数值的二进制特点,既省去重新计算 hash 的时间,又把之前冲突的节点散列到了其他位置

此外,HashMap 是非线程安全的,线程间的竞争条件主要是发生冲突或扩容时,链表的断链和续链操作。扩容也就意味着内存拷贝,这是一个很耗费性能的操作,所以预分配一个足够大的初始容量,减少扩容的次数,能够让 HashMap 有更好的表现。

HashMap的get()方法

get()方法也是在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
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) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//计算存放在数组table中的位置.具体计算方法上面也已经介绍了
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//先查找是不是就是数组中的元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//该位置为红黑树根节点或者链表头结点
if ((e = first.next) != null) {
//如果first为红黑树结点,就在红黑树中遍历查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//不是树结点,就在链表中遍历查找
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

根据key算出hash值定位到哈希桶的索引,当可以就是当前索引的值则直接返回其对于的value,反之用key去遍历equal该索引下的key,直到找到位置。如图所示:

get

HashMap的remove()方法

最后还有一个基本的方法,那就是remove方法了。

1
2
3
4
5
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}

这个remove方法实际上调用的是removeNode方法,而它的参数和putVal方法中非常的类似。

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

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; // 声明节点数组、当前节点、数组长度、索引值
/*
* 如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空
* 需要从该节点p向下遍历,找到那个和key匹配的节点对象
*/
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; // 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量

// 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;

/*
* 到这一步说明首节点没有匹配上,那么检查下是否有next节点
* 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了
* 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
*/
else if ((e = p.next) != null) {
// 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可
else {
do {
// 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}

// 走到这里,说明e也没有匹配上
p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
} while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
}
}

/*
* 如果node不为空,说明根据key匹配到了要删除的节点
* 如果不需要对比value值 或者 需要对比value值但是value值也相等
* 那么就可以删除该node节点了
*/
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点
tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可
else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
p.next = node.next;
++modCount; // HashMap的修改次数递增
--size; // HashMap的元素个数递减
afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
return node;
}
}
return null;
}

删除操作就是一个查找+删除的过程,它的步骤也很简单,就先根据hashcode值,找到bucket的位置,找到位置之后,在节点中根据key的值,找到对应的value。

总结

  1. HashMap是基于哈希表实现的,用Entry[]来存储数据,而Entry中封装了key、value、hash以及Entry类型的next
  2. HashMap存储数据是无序的
  3. hash冲突是通过拉链法解决的
  4. HashMap的容量永远为2的幂次方,有利于哈希表的散列
  5. HashMap不支持存储多个相同的key,且只保存一个key为null的值,多个会覆盖
  6. put过程,是先通过key算出hash,然后用hash算出应该存储在table中的index,然后遍历table[index],看是否有相同的key存在,存在,则更新value;不存在则插入到table[index]单向链表的表头,时间复杂度为O(n)
  7. get过程,通过key算出hash,然后用hash算出应该存储在table中的index,然后遍历table[index],然后比对key,找到相同的key,则取出其value,时间复杂度为O(n)

方法摘要

void clear()
从此映射中移除所有映射关系。
Object clone()
返回此 HashMap 实例的浅表副本:并不复制键和值本身。
boolean containsKey(Object key)
如果此映射包含对于指定键的映射关系,则返回 true。
boolean containsValue(Object value)
如果此映射将一个或多个键映射到指定值,则返回 true。
Set<Map.Entry<K,V>> entrySet()
返回此映射所包含的映射关系的 Set 视图。
V get(Object key)
返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。
boolean isEmpty()
如果此映射不包含键-值映射关系,则返回 true。
Set keySet()
返回此映射中所包含的键的 Set 视图。
V put(K key, V value)
在此映射中关联指定值与指定键。
void putAll(Map<? extends K,? extends V> m)
将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。
V remove(Object key)
从此映射中移除指定键的映射关系(如果存在)。
int size()
返回此映射中的键-值映射关系数。
Collection values()
返回此映射所包含的值的 Collection 视图。