程序员:HashMap讲解,分析扩容机制源码

一只刚刚上路的猿 2019-12-26

具体看源码之前,我们先简单的说一下HashMap的底层数据结构

1、HashMap底层的数据结构是 数组 + 链表 + 红黑树

2、我们需要先了解一下HashMap底层的两个变量

2-1:loadFactor: 加载因子,默认是0.75,这个值是经过反复测试最合适的值。

2-2:threshold: 当map里面的数据大于这个threshold就会进行扩容

程序员:HashMap讲解,分析扩容机制源码

现在来看一下HashMap的构造方法

hashMap的构造方法有4个。

1、空参构造方法,这个时候加载因子为默认的0.75,并且不会创建空间。

threshold 为0

数组为null

public HashMap() {

this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

}

2、给定初始化容量大小,这个构造方法里面会直接去调用第三个构造方法

threshold 已经有值了

数组为null

public HashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR);

}

3、给定初始化大小,和加载因子。

1、其实并不建议修改默认的加载因子。当然除非你很了解这里面的逻辑找到一个适合自己这个项目的加载因子

2、先是判断你给的初始化容量是否合法,如果合法的话就用这个初始化容量计算出 threshold

threshold 已经有值了

数组为null

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))

throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

this.loadFactor = loadFactor;

this.threshold = tableSizeFor(initialCapacity);

}

4、把一个Map作为参数传递过来,加载因子适应默认的0.75。把其它Map转化成HashMap

threshold 已经有值了

数组为也不为空了

public HashMap(Map<? extends K, ? extends V> m) {

this.loadFactor = DEFAULT_LOAD_FACTOR;

putMapEntries(m, false);

}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {

int s = m.size();

if (s > 0) {

if (table == null) { // pre-size

float ft = ((float)s / loadFactor) + 1.0F;

int t = ((ft < (float)MAXIMUM_CAPACITY) ?

(int)ft : MAXIMUM_CAPACITY);

if (t > threshold)

threshold = tableSizeFor(t);

}

else if (s > threshold)

resize();

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);

}

}

}

让我们看看是HashMap是怎么进入扩容的

3-1:我们先从 put() 这个方法说起

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

这个put方法底层是调用了一个叫 putVal 的方法,但是在这之前我们有必要看一下hash()这个方法。

直接使用 对象.hashCode(), 可能会出现重复,所以这个hash是对生成的hashcode进行一下扰乱,让其重复性更低。

从这里也可以看到,HashMap只允许一个null键

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

3-2:下面我们看一下这个putVal方法

putVal源码:

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;

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

else {

Node<K,V> e; K k;

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

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) {

p.next = newNode(hash, key, value, null);

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;

}

}

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;

}

这个源码看起来还是有点复杂的,考虑到很多同学可能和我一样数据结构并不是太好。我把它简化一下,提取里面的思想便于理解

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){

// 当数据为null或者长度为0的时候进行扩,并发扩后的长度返回给n(前面说了hashMap底层最开始是个数组)

n = (tab = resize()).length;

}

// 之前可能有同学有疑问,hashcode那么长,为啥默认HashMap数组默认长度是16。其实最后的下标是经过处理的 (n - 1) & hash

if ((p = tab[i = (n - 1) & hash]) == null){

// 如果当前数组的下标,并没有数据,也就是说当前添加的数据是第一个,那就直接加入进去就好了。不需要排序啥的

tab[i] = newNode(hash, key, value, null);

}

else {

// 找到了数据下标,并且里面的已经有数据了,

// 这里就要找到当前数据的位置属于那里并加入进去,

// 还要判断当前长度是否大于我们设置的长度,大于就要把链转化成红黑树便于查找

}

++modCount;

// 判断当前长度是否大于需要扩的长度,其实也好理解,数组是可以装满的,但是链不可能满呀,但是长度超过一定的长度的时候链的性能就会很差了

if (++size > threshold)

resize();

// 节点插入后的操作,目前这个没有任何实现,里面是个空方法

afterNodeInsertion(evict);

return null;

}

3-3:总结进入扩容的两种情况

添加一个数据的时候,底层数组为空的时候

添加一个数据结束后,判断当前数据个数是否大于threshold (需要扩容)的大小,大于就进行扩容

注:因为数据是具体添加到数组里面的链表,所以不存在数组越界情况。

具体看一下扩容代码

扩容源码:

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;

}

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<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;

}

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、找到新的容量大小和新的threshold大小

// 2、把旧的数据全部复制到新的数组中去

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;

}

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)

newThr = oldThr << 1; // double threshold

}

// 使用初始化容量或初始化容量+初始化加载因子参数的构造方法,第一次进入扩容

else if (oldThr > 0){

newCap = oldThr;

}

// 使用空参构造方法第一次扩容进入,使用参数为map的构造方法,第一次也会进入这个扩容方法

else {

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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

table = newTab;

// 把旧的数据全部复制到新的数组中去

if (oldTab != null) {

//

}

return newTab;

}

总结(面试的时候:请你说一下HashMap的扩容):

HashMap底层数据结构是 数组 + 链表 + 红黑树

真正的数据是存储在链表中的,链表的长度是无限的。所以这时候就引入一个变量 threshold

当第一次向map里面添加数据,或添加完数据后的大小,大于 threshold的大小,这时候就会进行扩容

先说一下非第一次扩容,这个相对简单点

1、如果当前的容量大小,大于等于HashMap规定的最大容量的话,直接让threshold等于Integer的最大值,就可以了。

2、一般情况当前数组长度是不会大于最大值的,所以这时候新的数组长度等于旧数组的2倍。如果新的数组长度小于HashMap规定的最大值,并且旧的数组长度也大于等于HashMap规定的默认大小容量大小(16),那么threshold扩大2倍,否则不变

非第一次扩容

1、HashMap,有四个构造方法。空参构造方法的threshold变量是0,其它构造方法threshold都有初始值。

2、当旧的threshold大于0的时候,新的数组容量大小就等于旧的threshold大小。新的threshold大小等于加载因子新的数组大小。

3、当旧的threshold不大于0的时候,新的数组大小就等于默认的大小(16),新的threshold大小,就等于默认的容量大小默认的加载因子大小

上面已经得出了新的容量大小和新的threshold的大小,后面只需要用新容量大小创建一个数组,把旧数组的内容复制进去就好了。

程序员:HashMap讲解,分析扩容机制源码


原文链接:https://blog.csdn.net/Tomwildboar/article/details/103656006

相关推荐