阅读 83

HashMap总结

JDK1.7中的HashMap

重要成员属性
// 默认初始容量 初始容量必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大hash表容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子(取了在时间和空间上一个比较不错的均衡,不一定是0.75,不同的hashmap实现也存在不同的值)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 空hash表
static final Entry<?,?>[] EMPTY_TABLE = {};
// hash表,需要扩容长度必须始终是 2 的幂。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
复制代码
构造函数
Map<String, String> map = new HashMap<String, String>();
或
Map<String, String> map = new HashMap<String, String>(11);
复制代码

image.png
如果不传入参数默认的初始容量为16,加载因子为0.75。
image.png
如果带有初始化参数,上面说过初始容量必须是2的次幂,但是传入的如果不是2的次幂呢?

put()方法

image.png
在hashmap的put()方法中,首先会判断如果为空的hash表,则会进行初始化操作。
image.png
在进行初始化操作的时候,会对传入的初始容量进行转换成离传入参数最近的那个2的次幂的数值
接着往下,会对key进行求hash值。 image.png
image.png
长度必须是 2 的非零次幂,这里为什么要用length-1?

h      = 0001 0101 0111 0010 1111
length = 0000 0000 0000 0001 0000 (16不减1) 结果只有两种0,16
length = 0000 0000 0000 0000 1111 (16减1=15) 结果有16种,更加散列
复制代码

image.png
紧接着,for循环,对相同的key的value会进行替换。
image.png
接着就进入重要的方法里面,在这里会进行对hash表的扩容。 image.png
判断当前hash表的容量,是否大于阈值threshold(capacity * 扩容比率0.75),就进行扩容。扩容是以当前hash表的容量* 2的方式进行扩容。即创建一个新的数组。
image.png
扩容后,有了新的数组,就要开始转移数据,在transfer()方法中实现。
image.png
在transfer()方法中转移数据,循环遍历需要重新对key进行hash计算,也就是key在旧的数组中的位置与新的数组中的位置不一定相同。然后将旧的数组中的数据移动到新的数组中去。

单线程扩容

假设:hash算法就是简单的key与length(数组长度)求余。hash表长度为2,如果不扩 容, 那么元素key为3,5,7按照计算(key%table.length)的话都应该碰撞到table[1]上。
扩容:hash表长度会扩容为4重新hash,key=3 会落到table[3]上(3%4=3), 当前 e.next为key(7), 继续while循环重新hash,key=7 会落到table[3]上(7%4=3), 产生碰撞, 这里采用的是头插入法,所以key=7的Entry会排在key=3前面(这里可以具体看while语句 中代码)当前e.next为key(5), 继续while循环重新hash,key=5 会落到table[1]上 (5%4=3), 当前e.next为null, 跳出while循环,resize结束。
image.png

多线程扩容
while(null != e) {
 Entry<K,V> next = e.next;//第一行,线程1执行到此被调度挂起
 int i = indexFor(e.hash, newCapacity);//第二行
 e.next = newTable[i];//第三行
 newTable[i] = e;//第四行
 e = next;//第五行
 }
复制代码

image.png 从上面的图可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程 2 rehash 后,就指向了线程2 rehash 后的链表。然后线程1被唤醒了:

  1. 执行e.next = newTable[i],于是 key(3)的 next 指向了线程1的新 Hash 表,因 为新 Hash 表为空,所以e.next = null,
  2. 执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。
  3. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

然后该执行 key(3)的 next 节点 key(7)了:

  1. 现在的 e 节点是 key(7),首先执行Entry next = e.next,那么 next 就是 key(3)了
  2. 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7) 4. 执行e = next,将 e 指向 next,所以新的 e 是 key(3)

此时状态为:
image.png
然后又该执行 key(7)的 next 节点 key(3)了:

  1. 现在的 e 节点是 key(3),首先执行Entry next = e.next,那么 next 就是 null
  2. 执行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

这时候的状态如图所示:
image.png
出现了环形链表。

JDK1.8中的HashMap

重要成员属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转红黑树时hash表最小容量阈值,达不到优先扩容
static final int MIN_TREEIFY_CAPACITY = 64;
复制代码

在jdk1.8中,对hashmap进行了优化,引入了红黑树,在链表过长的时候,也就是阈值为8,不是代表链表的长度,表示链表长度大于8的时候也就是9的时候转红黑树,而且转红黑树还有一个条件,即数组容量大于等于64的时候才会转红黑树,否则优先扩容。为什么链表转红黑树的阈值是8?

(exp(-0.5) * pow(0.5, k) / factorial(k)).

0:    0.60653066
1:    0.30326533
2:    0.07581633
3:    0.01263606
4:    0.00157952
5:    0.00015795
6:    0.00001316
7:    0.00000094
8:    0.00000006
more: less than 1 in ten million
复制代码

泊松分布,链表达到8的概率比较低,因此是8。

put()方法

image.png
对比可以发现,jdk1.8对hashmap进行了优化。先来看看转红黑树的条件。 image.png
image.png
可以发现,如果数组的长度小于64则优先进行扩容。Java8 HashMap扩容跳过了Jdk7扩容的坑,对源码进行了优化,采用高低位拆分转移方式,避免了链表环的产生。

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;      //移到新的数组上的同样的index位置
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
复制代码

从源码看会发现完全绕开rehash操作,但是要满足高低位移动,必须数组容量是2的幂次方。
如图所示: image.png
image.png
jdk8中put方法过程图: image.png

文章分类
后端
文章标签