HashMap分析

1.概述

HashMap位于java.util包中,HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null键和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。它的底层为哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。JDK1.8之后,当链表长度超过 8 时,链表转换为红黑树

HashMap主要属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
//HashMap的初始容量大小为16(1<<4),指的是存储元素的数组大小,即桶的数量
//为啥是16呢? 因为在使用2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这是为了实现均匀分布。
static final int DEFAULT_INITIAL_CAPACITY = 16;
//HashMap的最大容量(1<<30)
//使用位与运算速度更快
static final int MAXIMUM_CAPACITY = 1073741824;
//默认负载因子0.75
//当HashMap中的数据量/HashMap的总容量=0.75或者指定值时,HashMap的总容量自动扩展一倍
//负载因子代表了hash表中元素的填满程度。加载因子越大,填满的元素越多,但是冲突的机会增大了,链表越来越长,查询速度会降低。反之,如果加载因子过小,冲突的机会减小了,但是hash表过于稀疏。冲突越大,查找的成本就越高。
static final float DEFAULT_LOAD_FACTOR = 0.75F;
//由链表转换成树的阈值:8
//在存储数据时,当某一个桶中链表的长度>=8时,链表结构会转换成红黑树结构(其实还要求桶的中数量>=64)
static final int TREEIFY_THRESHOLD = 8;
//红黑树转为链表的阈值:6
//当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量<6时,则将红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形化容量阈值:64
//当哈希表中的容量>该值时,才允许链表转换成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
......
}

2.Node

JDK 1.8采用的是Node数组,实质上还是Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题。
一个桶中链表的长度<8时:

一个桶中链表的长度>=8时:


数组存储区间是连续的,占用内存严重,故空间复杂度很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难
链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(n)。链表的特点是:寻址困难,插入和删除容易
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
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

3.hash算法

首先获取对象的hashCode()值,然后将hashCode值右移16位,然后将右移后的值与原来的hashCode做异或(^)运算,返回结果。(其中h>>>16,在JDK1.8中,优化了高位运算的算法,使用了零扩展,无论正数还是负数,都在高位插入0)

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

4.put()

HashMap并没有直接提供putVal接口给用户调用,而是提供的put方法,而put方法就是通过putVal来插入元素的。

  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向6,如果table[i]不为空,转向3;
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向4,这里的相同指的是hashCode以及equals;
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
    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
       public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
    }

    //put函数的核心处理函数
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //HashMap是懒加载,所有的put先检查table数组是否已经初始化,没有初始化则进行扩容数组初始化table数组,保证table数组一定初始化
    if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
    //通过hash算法(n - 1) & hash找到数组下标得到数组元素,为空则新建
    //(n-1)&hash就等价于hash%n。&运算的效率高于%运算。
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
    else {
    Node<K,V> e; K k;
    //找到数组元素,hash相等同时key相等,则直接覆盖,执行赋值操作
    if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    e = p;
    // hash值不相等,即key不相等
    //判断链表是否是红黑树
    else if (p instanceof TreeNode)
    //放入树中
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    else {
    //遍历当前的链表,一直遍历到链表末尾
    for (int binCount = 0; ; ++binCount) {
    //到达链表的尾部
    if ((e = p.next) == null) {
    //在尾部插入结点(JDK1.8之前采用头插法,JDK1.8之后采用尾插法,使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环)
    p.next = newNode(hash, key, value, null);
    //当链表长度超过8(阈值),就会将链表便转化为红黑树
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    treeifyBin(tab, hash);
    break;
    }
    if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    break;
    p = e;
    }
    }
    //表示在桶中找到key值、hash值与插入元素相等的结点
    if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
    //用新值替换旧值
    e.value = value;
    afterNodeAccess(e);
    return oldValue;
    }
    }
    //记录修改次数
    ++modCount;
    //判断是否需要扩容
    if (++size > threshold)
    resize();
    afterNodeInsertion(evict);
    return null;
    }
    HashMap的数据存储实现流程
  7. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16)
  8. 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了
    ① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
    ② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作;
    ③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:
    如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。
    注意:
    HashMap的put会返回key的上一次保存的数据,比如:
1
2
3
4
HashMap<String, String> map = new HashMap<String, String>();
System.out.println(map.put("a", "A")); // 打印null
System.out.println(map.put("a", "AA")); // 打印A
System.out.println(map.put("a", "AB")); // 打印AA

5.get()

HashMap同样并没有直接提供getNode接口给用户调用,而是提供的get方法,而get方法就是通过getNode来取得元素的。

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
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已经初始化,长度大于0,根据hash寻找table中的项也不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//永远检查第一个node,桶中第一项(数组元素)相等
//在Hashmap1.8中,无论是存元素还是取元素,都是优先判断bucket上第一个元素是否匹配,而在1.7中则是直接遍历查找
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//一次就匹配到了,直接返回,
//否则进行搜索
return first;
if ((e = first.next) != null) {
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;
}

基本流程:

  1. 根据key计算hash;
  2. 检查数组是否为空,为空返回null;
  3. 根据hash计算bucket位置,如果bucket第一个元素是目标元素,直接返回。否则执行4;
  4. 如果bucket上元素大于1并且是树结构,则执行树查找。否则执行5;
  5. 如果是链表结构,则遍历寻找目标

6.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
83
84
85
86
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//定义了一个临时Node类型的数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;//判断目前的table数组是否为空,记录下当前数组的大小
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
//如果已达到最大容量不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//通过位运算扩容到原来的两倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//用构造器初始化了阈值,将阈值直接赋值给容量
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//初始化新的Node类型数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新数组的值复制给旧的hash桶数组
table = newTab;
//当原来的table不为空,需要将数据迁移到新的数组里面去
if (oldTab != null) {
//开始对原table进行遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//取出这个数组第一个不为空的元素
if ((e = oldTab[j]) != null) {
//将旧的hash桶数组在j结点处设置为空,把空间释放掉,方便gc
oldTab[j] = null;
//如果e后面没有Node结点
if (e.next == null)
//计算相应的hash值,把节点存储到新table的对应位置处
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)///如果e是红黑树的类型,按照红黑树的节点移动
((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;//将Node结点的next赋值给next
//如果结点e的hash值与原hash桶数组的长度作与运算为0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//如果结点e的hash值与原hash桶数组的长度作与运算不为0
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;
}



7.面试题

  1. Object若不重写hashcode()的话,hashcode是如何计算出来的?
    Object的hashcode()方法是本地方法,该方法直接返回对象的内存地址,如果不重写hashcode(),则任何对象的hashcode都不相等。(然而hashmap想让部分值的hashcoe值相等,所以需要重写)
  2. 为什么重写equals()还要重写hashcode()?
    HashMap中比较key先求出key的hashcode(),比较其值是否相等,相等则比较equals(),若相等则认为它们是相等的,若equals()不相等则认为它们是不相等的。
    如果只重写equals()不重写hashcode(),就会导致相同的key值被认为不同(如果不重写hashcode(),则任何对象的hashcode都不相等),就会在HashMap中存储相同的key值(map中key值不能相同)。
    ①如果两个对象相同(equals()比较返回true),那么它们的hashcode值一定相同;
    ②如果两个对象的hashcode值相同,它们并不一定相同(equals()比较返回false)
请作者喝瓶肥宅快乐水