HashMap源码解析

5.1、对于HashMap需要掌握以下几点

  • Map的创建:HashMap()
  • 往Map中添加键值对:即put(Object key, Object value)方法
  • 获取Map中的单个对象:即get(Object key)方法
  • 删除Map中的对象:即remove(Object key)方法
  • 判断对象是否存在于Map中:containsKey(Object key)
  • 遍历Map中的对象:即keySet(),在实际中更常用的是增强型的for循环去做遍历
  • Map中对象的排序:主要取决于所采取的排序算法

5.2、构建HashMap

 源代码:

一些属性:

    static final int DEFAULT_INITIAL_CAPACITY = 16;    // 默认的初始化容量(必须是2的多少次方)
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大指定容量为2的30次方
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的加载因子(用于resize) transient Entry[] table;// Entry数组(数组容量必须是2的多少次方,若有必要会扩容resize)--这就是HashMap的底层数据结构 transient int size; // 该map中存放的key-value对个数,该个数决定了数组的扩容(而非table中的所占用的桶的个数来决定是否扩容)
// 扩容resize的条件:eg.capacity=16,load_factor=0.75,threshold=capacity*load_factor=12,即当该map中存放的key-value对个数size>=12时,就resize)
int threshold;
final float loadFactor; // 负载因子(用于resize) transient volatile int modCount;// 标志位,用于标识并发问题,主要用于迭代的快速失败(在迭代过程中,如果发生了put(添加而不是更新的时候)、remove操作,该值发生变化,快速失败)

注意:

  • map中存放的key-value对个数size,该个数决定了数组的扩容(size>=threshold时,扩容),而非table中的所占用的桶的个数来决定是否扩容
  • 标志位modCount采用volatile实现该变量的线程可见性(之后会在"Java并发"章节中去讲)
  • 数组中的桶,指的就是table[i]
  • threshold默认为0.75,这是综合时间和空间的利用率来考虑的,通常不要变,如果该值过大,可能会造成链表太长,导致get、put等操作缓慢;如果太小,空间利用率不足。

无参构造器(也是当下最常用的构造器)

    /**
* 初始化一个负载因子、resize条件和Entry数组
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;// 负载因子:0.75
threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//当该map中存放的key-value对个数size>=12时,就resize
table = new Entry[DEFAULT_INITIAL_CAPACITY];// 设置Entry数组容量为16
init();
}

注意:

  • init()为空方法

对于hashmap而言,还有两个比较常用的构造器,一个双参,一个单参。

 /**
* 指定初始容量和负载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity:"+initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//loadFactor<0或者不是一个值
throw new IllegalArgumentException("Illegal load factor:"+loadFactor); /*
* 下边的逻辑是找一个2的几次方的数,该数刚刚大于initialCapacity
* eg.当指定initialCapacity为17,capacity就是32(2的五次方),而2的四次方(16)正好小于17
*/
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;// capacity = capacity<<1 this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
} /**
* 指定初始容量
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);//调用上边的双参构造器
}

注意:

  • 利用上述两个构造器构造出的数组容量不一定是指定的初始化容量,而是一个刚刚大于指定初始化容量的2的几次方的一个值。
  • 在实际使用中,若我们能预判所要存储的元素的多少,最好使用上述的单参构造器来指定初始容量,这样的话,就可以避免就来扩容时带来的消耗(这一点与ArrayList一样

HashMap的底层数据结构是一个Entry[],Entry是HashMap的一个内部类,源代码如下:

static class Entry<K, V> implements Map.Entry<K, V> {
final K key; // 该Entry的key
V value; // 该Entry的value
Entry<K, V> next; // 该Entry的下一个Entry(hash冲突时,形成链表)
final int hash; // 该Entry的hash值 /**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K, V> n) {
value = v;
next = n;
key = k;
hash = h;
} public final K getKey() {
return key;
} public final V getValue() {
return value;
} //为Entry设置新的value
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry) o;
Object k1 = getKey();
Object k2 = e.getKey();
//在hashmap中可以存放null键和null值
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
} public final int hashCode() {
return (key == null ? 0 : key.hashCode())^(value == null ? 0 : value.hashCode());
} public final String toString() {
return getKey() + "=" + getValue();
}
}

注:这里我去掉了两个空方法。

  • Entry是一个节点,在其中还保存了下一个Entry的引用(用来解决put时的hash冲突问题),这样的话,我们可以把hashmap看作是"一个链表数组"
  • Entry类中的equals()方法会在get(Object key)中使用

5.3、put(Object key, Object value)

源代码:

put(Object key, Object value)

/**
* 向map中添加新Entry
* 步骤:
* 1)HashMap可以添加null的key,key==null的Entry只会放在table[0]中,但是table[0]不仅仅可以存放key==null的Entry
* 1.1、遍历table[0]中的Entry链,若有key==null的值就用新值覆盖旧值,并返回旧值value,
* 1.2、若无,执行addEntry方法,用新的Entry替换掉原来旧的Entry赋值给table[0],而旧的Entry作为新的Entry的next,执行结束后,返回null
* 2)添加key!=null的Entry时,
* 2.1、先计算key.hashCode()的hash值,
* 2.2、然后计算出将要放入的table的下标i,
* 2.3、之后遍历table[i]中的Entry链,若有相同key的值就用新值覆盖旧值,并返回旧值value,
* 2.4、若无,执行addEntry方法,用新的Entry替换掉原来旧的Entry赋值给table[i],而旧的Entry作为新的Entry的next,执行结束后,返回null
*/
public V put(K key, V value) {
/******************key==null******************/
if (key == null)
return putForNullKey(value); //将空key的Entry加入到table[0]中
/******************key!=null******************/
int hash = hash(key.hashCode()); //计算key.hashcode()的hash值,hash函数由hashmap自己实现
int i = indexFor(hash, table.length);//获取将要存放的数组下标
/*
* for中的代码用于:当hash值相同且key相同的情况下,使用新值覆盖旧值(其实就是修改功能)
*/
for (Entry<K, V> e = table[i]; e != null; e = e.next) {//注意:for循环在第一次执行时就会先判断条件
Object k;
//hash值相同且key相同的情况下,使用新值覆盖旧值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//e.recordAccess(this);
return oldValue;//返回旧值
}
} modCount++;
addEntry(hash, key, value, i);//增加一个新的Entry到table[i]
return null;//如果没有与传入的key相等的Entry,就返回null
}

注意:该方法头部的注释写明了整个put(Object key, Object value)的流程,非常重要

putForNullKey(V value)

/**
* 增加null的key到table[0]
*/
private V putForNullKey(V value) {
//遍历第一个数组元素table[0]中的所有Entry节点
for (Entry<K, V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {//用新值覆盖旧值
V oldValue = e.value;
e.value = value;
//e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//将新节点Entry加入到Entry[]中
return null;
}

addEntry(int hash, K key, V value, int bucketIndex)

/**
* 添加新的Entry到table[bucketIndex]
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
/*
* 这里可以看出,
* 1)新加入的Entry会放入链头,也就是说将来遍历的时候,最先加入map的反而是最后被遍历到的
* 2)采用的是Entry替换的方式
* 2.1、当添加第一个Entry1时,table[bucketIndex]==null,也就是说Entry1的下一个Entry为null(链尾),之后把table[bucketIndex] = Entry1
* 2.2、当添加第二个Entry2时,table[bucketIndex]==Entry1,也就是说Entry2的下一个Entry为Entry1,之后把table[bucketIndex] = Entry2
* 2.3、当添加第三个Entry3时,table[bucketIndex]==Entry2,也就是说Entry3的下一个Entry为Entry2,之后把table[bucketIndex] = Entry3
*/
Entry<K, V> e = table[bucketIndex];//新节点的下一个节点(当第一次在相应的数组位置放置元素时,table[bucketIndex]==null)
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
if (size++ >= threshold)//key-value对个数大于等于threshold
resize(2 * table.length);//扩容
}

注意:该方法头部的注释写明了该方法的流程示例,可以自己画个图对比着理解

hash(int h)

/**
* hash函数,用于计算key.hashCode()的hash值
* Note: null的key的hash为0,放在table[0].
*/
static int hash(int h) {
//这样的hash函数应该可以尽量将hash值打散
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
 

注意:在我们实际使用hashmap时,最好的情况是将key的hash值打散,使插入的这些Entry尽量落在不同的桶上(这样做的主要目的是提高查询效率),以上这个hash函数应该就是实现了这样的功能,但是为什么这样的hash函数可以将hash值打散,求大神指点!!!

indexFor(int h, int length)

/**
* "按位与"来获取数组下标
*/
static int indexFor(int h, int length) {
return h & (length - 1);
}

注意:hashmap始终将自己的桶保持在2的n次方,这是为什么?indexFor这个方法解释了这个问题。“这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率”--http://tech.meituan.com/java-hashmap.html

说明:在上述的addEntry(int hash, K key, V value, int bucketIndex)方法中,我们可以看到,当为把新的Entry赋值给table[i]后,会判断map中的key-value对是不是已经大于等于扩容条件值threshold了,若是,则需要调用resize函数,对Entry数组进行扩容,扩为原来二倍。

resize(int newCapacity)

/**
* 扩容步骤:
* 1)数组扩容为原来容量(eg.16)的二倍
* 2)将旧数组中的所有Entry重新计算索引,加入新数组
* 3)将新数组的引用赋给旧数组
* 4)重新计算扩容临界值threshold
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果旧的数组的容量为2的30次方(这种情况,不考虑了,如果真达到这样的情况,性能下降的就不像话了)
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];//newCapacity==2*oldCapacity
transfer(newTable);//将旧数组中的所有Entry重新计算索引,加入新数组
table = newTable;//将新数组赋给就数组
threshold = (int) (newCapacity * loadFactor);//重新计算threshold
}

transfer(Entry[] newTable)

jdk中的实现:

/**
* 将所有旧的数组中的所有Entry移动到新数组中去
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {//遍历旧数组
Entry<K, V> e = src[j];//获得头节点
if (e != null) {
/*
* 这样写,若同时有其他线程还在访问这个元素,则访问不到了,这里这样写,是考虑到多线程情况下,我们一般不会会用HashMap
* (查看ConcurrentHashMap并未将旧数组的值置为null)
* 这里将其置为null就方便gc回收
* 当然为了减小以上所说的影响,建议将src[j] = null;放在while循环结束后
*/
src[j] = null;
do {
Entry<K, V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//把之前已经存在的newTable[i]的元素赋给当前节点的下一个节点
newTable[i] = e;//把当前节点赋给newTable[i]
e = next;
} while (e != null);//遍历链表
}
}
}

我的修改:(注意:这是一个错误的修改,错误的根源在下边我会给出

/**
* 将所有旧的数组中的所有Entry移动到新数组中去
*/
void transfer(Entry[] newTable) {
Entry[] src = table; //旧数组
int newCapacity = newTable.length; //新数组容量 for (int j = 0; j < src.length; j++) {
Entry<K, V> e = src[j];//获取旧数组中的头节点Entry
if (e != null) {
src[j] = null;//将旧数组置空,让gc回收注意:这个时候table的桶并没有置空
/*
* 根据旧的hash值与新的容量值进行重新定位(注意:并没有重新计算hash值)
* 1、那么假设之前table[1]中存放的是Entry3,Entry3.next是Entry2,Entry2.next是Entry1,Entry1.next是null
* 那么假设重新计算后的i=3,那么Entry3-->Entry2-->Entry1依旧会在一起,都放入newTable[3],这样的话,我们只需要将链头的Entry3赋值给newTable[3]即可
* 2、既然通过indexFor(e.hash, newCapacity)不能把同一个桶下的Entry打散,为什么还要用呢?
* 主要是扩容后,若不用newCapacity去计算下标的话,那么扩容后,map中的Entry就都集中在了新数组的前半部分,这样就不够散了
*/
int i = indexFor(e.hash, newCapacity);
newTable[i] = e;//将Entry3赋值给newTable[3]
}
}
}

注意:

  • 在这个方法中,并没有重新计算hash值,只是重新计算了下标索引。
  • 错误根源在于认为同一个桶下的所有Entry的hash值相同,事实上不相同,只是hash&(table.length-1)的结果相同, 

    所以当table.length发生变化时,同一个桶下各个Entry算出来的index会不同(即Entry3、Entry2、Entry1可能会落在新数组的不同的桶上

5.4、get(Object key)

源代码:

get(Object key)

/**
* 查找指定key的value值
* 1、若key==null
* 遍历table[0],找出key==null的value,若没找到,返回null
* 2、若key!=null
* 1)计算key.hashCode()的hash值
* 2)根据计算出的hash值和数组容量,调用indexFor方法,获得table的下标i,进而获得桶table[i]
* 3)遍历该桶中的每一个Entry,找出key相等(==或equals)的Entry,获取此Entry的value即可
* 4)最后,若没有找到,返回null即可
*/
public V get(Object key) {
/****************查找key==null的value****************/
if (key == null)
return getForNullKey();
/****************查找key!=null的value****************/
int hash = hash(key.hashCode());//获取key.hashCode()的hash值 for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;//若没有指定key的Entry,则直接返回null
}

注意:查看代码头部的注释,表明了get的整个步骤

getForNullKey()

 /**
* 在table[0]中查询key==null
*/
private V getForNullKey() {
for (Entry<K, V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;//找不到的话就返回null
}

5.5、remove(Object key)

源代码:

/**
* 删除指定key的Entry
*/
public V remove(Object key) {
Entry<K, V> e = removeEntryForKey(key);
return (e == null ? null : e.value);//返回删除的节点(e为null的话,表示所给出的key不存在)
}
/**
* 删除指定key的Entry
* 1)若删除的是头节点,例如Entry3,只需将Entry2赋值给table[i]即可
* 2)若删除的是中间节点,例如Entry2,只需将Entry3.next指向Entry2.next(即Entry1)即可
* 3)若删除的是尾节点,例如Entry1,只需将Entry2.next指向Entry1.next(即null)即可
*/
final Entry<K, V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());//计算hash值
int i = indexFor(hash, table.length);//按位与计算下标
Entry<K, V> prev = table[i];//获取桶
Entry<K, V> e = prev; while (e != null) {
Entry<K, V> next = e.next;
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;//size-1
if (prev == e)//删除头节点,即示例中的Entry3
table[i] = next;
else//删除除了头节点外的其他节点
prev.next = next;
//e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;//返回删除的节点(e为null的话,表示所给出的key不存在)
}

注:看注释即可,最好用示例去套一下代码。

  • 若删除的key不存在于map中,返回null,不会抛异常。

5.6、containsKey(Object key)

源代码:

/**
* 判断map是否包含指定可以的Entry
*/
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
/**
* 判断map是否包含指定可以的Entry,与get(Object key)基本相同(只是这里将key==null与key!=null的情况写在了一起,get(Object key)也可以这样去做)
* 1)计算key.hashCode()的hash值
* 2)根据计算出的hash值和数组容量,调用indexFor方法,获得table的下标i,进而获得桶table[i]
* 3)遍历该桶中的每一个Entry,找出key相等(==或equals)的Entry,获取此Entry,并返回此Entry
* 4)最后,若没有找到,返回null即可
*/
final Entry<K, V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());//计算hash值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}

注意:此方法与get(Object key)基本相同,只是只是这里将key==null与key!=null的情况写在了一起,get(Object key)也可以这样去做来减少代码

5.7、keySet()

遍历所有Entry链表,获取每一个Entry的key,在整个过程中,如果发生了增删操作,抛出ConcurrentModificationException。

 final Entry<K, V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K, V> e = next;
if (e == null)
throw new NoSuchElementException(); if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}

总结:

  • HashMap底层就是一个Entry数组,Entry又包含next,事实上,可以看成是一个"链表数组"
  • 扩容:map中存放的key-value对个数size,该个数决定了数组的扩容(size>=threshold时,扩容),而非table中的所占用的桶的个数来决定是否扩容
  • 扩容过程,不会重新计算hash值,只会重新按位与
  • 在实际使用中,若我们能预判所要存储的元素的多少,最好使用上述的单参构造器来指定初始容量
  • HashMap可以插入null的key和value
  • remove(Object key):若删除的key不存在于map中,返回null,不会抛异常。
  • HashMap线程不安全,若想要线程安全,最好使用ConcurrentHashMap

疑问:

在我们实际使用hashmap时,最好的情况是将key的hash值打散,使插入的这些Entry尽量落在不同的桶上(这样做的主要目的是提高查询效率),以下这个hash函数应该就是实现了这样的功能,但是为什么这样的hash函数可以将hash值打散,求大神指点!!!

    static int hash(int h) {
//这样的hash函数应该可以尽量将hash值打散
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

jdk1.8对hashmap进行了改造,1.7中的hashmap最大的问题就是当链表比较长时,查询效率急剧下降;所以在1.8中,当链表长度>=8是,链表转为红黑树,提高查询效率。

1 JDK8 中的 HashMap与 JDK7 的 HashMap 有什么不一样?
JDK8中新增了红黑树,JDK8是通过数组+链表+红黑树来实现的
JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
JDK8中的因为使用了红黑树保证了插入和查询了效率,所以实际上JDK8中的Hash算法实现的复杂度降低了
JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否查过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值
JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容

2 HashMap 中 put 方法流程
在IDEA中我们找到HashMap源码中的put方法源码:

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

put方法调用了putVal方法,继续找到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
72
73
74
75
76
/**
* Implements Map.put and related methods
* @param hash key的哈希值
* @param key key值
* @param value 需要Put进去的value
* @param onlyIfAbsent 如果为true,不改变已存在的值
* @param evict 如果为false,则table 为creation 模式.
* @return 返回之前的值, 不存在则返回nullh
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//先判断table是否为空或者长度为零
if ((tab = table) == null || (n = tab.length) == 0)
//调用resize方法初始化数组
n = (tab = resize()).length;
//计算数组下标i=(tab.length - 1) & hash
if ((p = tab[i = (n - 1) & hash]) == null)
//如果数组下标处元素为空,则将hash, key, value等值封装成链表后保存在此数组下标处
tab[i] = newNode(hash, key, value, null);
else {
//数组下标处元素不为空
Node<K,V> e; K k;
//若p = tab[i = (n - 1) & hash]的hash值等于入参hash同时其key值等于入参key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//将tab[i = (n - 1) & hash]赋值给局部变量e
e = p;
//若数组下标处的元素是一棵红黑树
elseif (p instanceof TreeNode)
//调用TreeNode#putTreeVal方法并将其返回值赋值给e,putTreeVal方法后面再讲
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else //数组下标元素不同时满足其hash值等于入参hash和其key值等于入参key,
//同时它也不是红黑树节点,此时它仍然是Node类型的节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //若p的下一个节点为空,并把p.next赋值给e
//封装hash, key, value为一个新的Node节点赋值给p.next
p.next = newNode(hash, key, value, null);
//若binCount大于等于TREEIFY_THRESHOLD - 1
if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
//调用treeifyBin方法后跳出循环
treeifyBin(tab, hash);
break;
}
//若e的hash值等于入参hash,同时e的key值等于入参key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//跳出循环
break;
//以上两种情况都没出现时将e赋值给p
p = e;
}
}
if (e != null) { // 若对应的key值在HashMap中存在映射值
//将e的value值赋值给oldValue
V oldValue = e.value;
//若onlyIfAbsent==false或者oldValue == null
if (!onlyIfAbsent || oldValue == null)
//将新值value赋值给e.value
e.value = value;
//调用afterNodeAcces方法
afterNodeAccess(e);
//返回旧值
return oldValue;
}
}
//对应的key值在HashMap中不存在映射值,属于新增一个元素,modCount值加1
++modCount;
if (++size > threshold)
//若size+1后大于阈值threshold,则调用resize方法扩容
resize();
//调用afterNodeInsertion方法
afterNodeInsertion(evict);
//返回null
return null;
}

TreeNode#putTreeVal方法源码:

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
/**
* Tree version of putVal.
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//若当前TreeNode的父节点不为null则调用root()方法获得node节点,
//否则当前TreeNode即为root节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//从红黑树的root节点开始遍历
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//若p节点的hash值大于入参hash值h则dir赋值为-1
if ((ph = p.hash) > h)
dir = -1;
//若p节点的hash值小于入参hash值h则dir赋值为1
elseif (ph < h)
dir = 1;
//p.key赋值给pk
elseif ((pk = p.key) == k || (k != null && k.equals(pk)))
//若满足(pk = p.key) == k或者k != null && k.equals(pk)
//则返回p节点
return p;
//若满足kc为null同时调用comparableClassFor(k)方法的返回值赋给kc后的值也为null
//或者调用compareComparables(kc, k, pk)方法后的返回值赋值给dir后等于0
elseif ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
//若searched==false
TreeNode<K,V> q, ch;
searched = true;
//p.left或p.right赋值给ch后不为null,同时满足调用ch.find(h, k, kc)方法后的
//返回值赋值给q后也不为null则返回q节点
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//调用tieBreakOrder(k, pk)方法的返回值赋值给dir
dir = tieBreakOrder(k, pk);
}
//定义红黑树节点xp,并将p赋值给xp
TreeNode<K,V> xp = p;
//若dir <= 0则将p.left赋值给p,否则将p.right赋值给p,以进行下一次遍历
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//p节点为null走以下逻辑
//定义xpn节点,并将xp.next节点赋值给xpn
Node<K,V> xpn = xp.next;
//定义x节点为调用map.newTreeNode(h, k, v, xpn)方法后的返回值
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
//若dir<=0 则将x节点赋值给xp的左孩子节点,否则将x节点赋值给xp的右孩子节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//将x节点赋值给xp的next节点
xp.next = x;
//将xp节点同时赋值给x的prev节点和parent节点
x.parent = x.prev = xp;
if (xpn != null)
//若xpn节点不为null则将x节点赋值给xpn的prev节点
((TreeNode<K,V>)xpn).prev = x;
//确保root节点是根节点,里面调用了插入节点后的平衡红黑树方法balanceInsertion(root, x)
moveRootToFront(tab, balanceInsertion(root, x));
//返回null
return null;
}
}
}

以上涉及到红黑树中的复杂算法待自己搞明白了红黑树数据结构再另外撰写一篇文章发布

综上,JDK8中HashMap的put操作流程如下:

1) 对Key求Hash值,然后再计算下标:

2)如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)
3)如果碰撞了,以链表的方式链接到后面

4)如果链表长度超过阀值( TREEIFY_THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表

5)如果节点已经存在就替换旧值

6)如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

3 HashMap 的 get 方法流程
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

4 HashMap 扩容流程是怎样的?
1)HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新数组上来,这样才是数组的扩容

2)在HashMap中也是一样,先新建一个2被数组大小的数组

3)然后遍历老数组上的每一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去

4)在这个过程中就需要遍历链表,当然jdk7,和jdk8在实现时是不一样的,jdk7就是简单的遍历链表上的每一个元素,然后按每个元素的hashcode结合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率

5)而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置

6)元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收到垃圾收集器中

5 谈谈你对红黑树的理解

1)每个节点非红即黑

2)根节点总是黑色的

3)如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

4)每个叶子节点都是黑色的空节点(NIL节点)

5)从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

为什么 HashMap 的数组的大小是2的幂次方数?
当某个key-value对需要存储到数组中时,需要先生成一个数组下标index,并且这个index不能越界。

在HashMap中,先得到key的hashcode,hashcode是一个数字,然后通过hashcode & (table.length - 1) 运算得到一个数组下标index,是通过位运算中的与运算计算出来一个数组下标的,而不是通过取余,与运算比取余运算速度更快,但是也有一个前提条件,那就是数组的长度必须是一个2的幂次方数。

HashSet源码解析

6.1、对于HashSet需要掌握以下几点

  • HashSet的创建:HashSet()
  • 往HashSet中添加单个对象:即add(E)方法
  • 删除HashSet中的对象:即remove(Object key)方法
  • 判断对象是否存在于HashSet中:containsKey(Object key)

 注:HashSet没有获取单个对象的方法,需要使用iterator

6.2、构建HashSet

源代码:

//HashSet底层数据结构:通过hashmap的key不可重复的原则,使得存放入HashSet中的值不重复
private transient HashMap<E, Object> map;
//默认的hashmap的value
private static final Object PRESENT = new Object();
/**
* 可存放16个元素
*/
public HashSet() {
map = new HashMap<E, Object>();
}
/**
* 指定hashset的容量和负载因子
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E, Object>(initialCapacity, loadFactor);
}
/**
* 指定hashset的容量
*/
public HashSet(int initialCapacity) {
map = new HashMap<E, Object>(initialCapacity);
}

注:HashSet的底层是HashMap,其依靠HashMap的key不可重复,来保证将来加入到HashSet中的元素也不重复(会将元素作为key放到hashmap中,参照6.3)。

6.3、add(E e)

源代码:

add(E e)

/**
* 往set中添加值
*/
public boolean add(E e) {
//查看hashmap的put方法,若覆盖已有key的旧值,会返回旧值;若没有相应的key则返回null
return map.put(e, PRESENT) == null;
}

注意:这里调用了HashMap的put(K key, V value)

6.4、remove(Object key)

源代码:

 /**
* 删除指定元素
*/
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}

注:这里调用了HashMap的remove(Object key)

6.5、contains(Object key)

源代码:

/**
* set中是否包含指定元素
*/
public boolean contains(Object o) {
return map.containsKey(o);
}
 

注意:这里调用了HashMap的containsKey(Object key)

总结:

  • HashSet底层就是HashMap
  • 其依靠HashMap的key不可重复,来保证将来加入到HashSet中的元素也不重复(会将元素作为key放到hashmap中)
  • HashSet线程不安全

7.1、List(允许重复元素)

  • ArrayList:

    • 底层数据结构:Object[]
    • 查询(get)、遍历(iterator)、修改(set)使用的比较多的情况下,用ArrayList
    • 可扩容,容量无限
  • LinkedList
    • 底层数据结构:环形双向链表
    • 增加(add)、删除(remove)使用比较多的情况下,用LinkedList
    • 链表,容量无限

说明:

1)add(E e):在数组末尾插入元素,ArrayList需要考虑扩容问题,一旦扩容就要进行数组复制,LinkedList不需要;

2)add(int index):在数组中间插入元素,ArrayList需要考虑将该index及其后的数组元素全部复制后移一位,LinkedList不需要

7.2、Set(不允许重复元素,所以可用于去重操作)

  • HashSet:

    • 底层数据结构:HashMap
    • 可看做容量无限
  • TreeSet:
    • 底层数据结构:TreeMap
    • 容量无限

7.3、Map(key-value)

  • HashMap:

    • 底层数据结构:链表数组
    • 可扩容,且最大容量极大,可看做容量无限
  • TreeMap:
    • 底层数据结构:红黑树
    • 可以实现按key排序(在使用中,要么使用TreeMap(Comparator),要么让key对象实现Comparable)
    • 红黑树,容量无限

注意:

  • 以上全部线程不安全
  • 对于查找和删除较为频繁,且元素数量较多(元素数量>100)的情况下,Set和Map性能要比List好一些(单线程情况下)

第十三章 HashMap&HashSet源码解析的更多相关文章

  1. 第六章 HashSet源码解析

    6.1.对于HashSet需要掌握以下几点 HashSet的创建:HashSet() 往HashSet中添加单个对象:即add(E)方法 删除HashSet中的对象:即remove(Object ke ...

  2. 【Java集合】HashSet源码解析以及HashSet与HashMap的区别

    HashSet 前言 HashSet是一个不可重复且元素无序的集合.内部使用HashMap实现. 我们可以从HashSet源码的类注释中获取到如下信息: 底层基于HashMap实现,所以迭代过程中不能 ...

  3. 给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器

    今天的主角是HashSet,Set是什么东东,当然也是一种java容器了.      现在再看到Hash心底里有没有会心一笑呢,这里不再赘述hash的概念原理等一大堆东西了(不懂得需要先回去看下Has ...

  4. HashSet源码解析

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 6.1.对于HashSet需要掌握以下几点 HashSet的创建:HashSet() 往HashSet中添加 ...

  5. .NET Core实战项目之CMS 第三章 入门篇-源码解析配置文件及依赖注入

    作者:依乐祝 原文链接:https://www.cnblogs.com/yilezhu/p/9998021.html 写在前面 上篇文章我给大家讲解了ASP.NET Core的概念及为什么使用它,接着 ...

  6. java集合-HashSet源码解析

    HashSet 无序集合类 实现了Set接口 内部通过HashMap实现 // HashSet public class HashSet<E> extends AbstractSet< ...

  7. Java - HashSet源码解析

    java提高篇(二四)-----HashSet 一.定义 public class HashSet<E> extends AbstractSet<E> implements S ...

  8. HashSet源码解析笔记

    HashSet是基于HashMap实现的.HashSet底层采用HashMap来保存元素,因此HashSet底层其实比较简单. HashSet是Set接口典型实现,它按照Hash算法来存储集合中的元素 ...

  9. java基础,集合,HashMap,源码解析

    最怕,你以为你懂咯,其实你还不懂: 见贤思齐,看看那些我们习以为常的集合,通过相关定义.源码,思考分析,加深对其的理解,提高编码能力,能做一个略懂的程序员: 做几个我们常用的集合类.开篇HashMap ...

  10. Java泛型底层源码解析-ArrayList,LinkedList,HashSet和HashMap

    声明:以下源代码使用的都是基于JDK1.8_112版本 1. ArrayList源码解析 <1. 集合中存放的依然是对象的引用而不是对象本身,且无法放置原生数据类型,我们需要使用原生数据类型的包 ...

随机推荐

  1. mysql弱密码爆破

    mySQL弱密码  靶场:/vulhub/mysql/CVE-2012-2122  启动: docker-compose up -d 扫描端口 nmap -Sv -Pn -T4 靶机ip  看到在33 ...

  2. 3.11 Linux删除空目录(rmdir命令)

    和 mkdir 命令(创建空目录)恰好相反,rmdir(remove empty directories 的缩写)命令用于删除空目录,此命令的基本格式为: [root@localhost ~]# rm ...

  3. 模拟器(Nintendo,Genesis,SFC,MD,土星,PS,PS2,PS3,Wii,Xbox等)游戏下载网址

    最近想拿个英文游戏复习复习,国内的emu618关闭之后难得寻到很完整的游戏库 通过 https://www.fantasyanime.com/mana/som2downloads.htm 找到 htt ...

  4. paramiko模块的使用

    简介: Paramiko是基于Python(2.7,3.4+)版本实现和封装了SSHv2协议,底层是用cryptography实现,我们如果希望远程登录主机或者远程下载或者上传文件到远程主机都可以使用 ...

  5. JAVA 注解示例 详解

    注解(Annotation) 为我们在代码中天界信息提供了一种形式化的方法,是我们可以在稍后 某个时刻方便地使用这些数据(通过 解析注解 来使用这些数据). 注解的语法比较简单,除了@符号的使用以外, ...

  6. MySQL数据库设计规范(新)

    目录 规范背景与目的 设计规范2.1 数据库设计2.1.1 库名2.1.2 表结构2.1.3 列数据类型优化2.1.4 索引设计2.1.5 分库分表.分区表2.1.6 字符集2.1.7 程序DAO层设 ...

  7. OS之《进程管理》

    进程同步 同步实际上是指:将多个进程,按照顺序,有序执行. 让进程有序进行的场景有很多.比如:一个进程依赖另外一个进程的计算结果:一个进程等待另外一个对临界资源的访问:还有像生产者消费者模型中的相互配 ...

  8. 对象存储 AVIF 图片压缩,即将公测!

    2021年8月,腾讯云数据万象以内测方式推出了最前沿的 AVIF 图片压缩服务,可以在图片主观质量相同的情况下大幅降低码率,节省储存空间. 经过3个月时间的内测,我们收集到了很多热心用户的反馈,AVI ...

  9. R数据分析:国产新冠口服药比辉瑞好的文章的统计做法分享

    元旦前在人民日报中央厨房上看到一篇文章,叫做"比肩辉瑞的国产新冠药物VV116,是这样研制和临床试验的",想来就把文献原文找来读了读,写下本文分享给大家,本文主要关注文章的正文中主 ...

  10. Elm 和 Jetpack Compose 殊途同归及 MVVM 缺点分析

    Html.lazy · An Introduction to Elm 可能搞 vdom 的都会到 lazy renderer 这一步,react 可能也可以这么搞或者已经这么搞了我不知道,Elm 提到 ...