jdk1.8 HashMap源码解析(上)

1、HashMap概述

Map 是 Key-Value 对映射的抽象接口,该映射不包括重复的键,即一个键对应一个值。HashMap 是 Java Collection Framework 的重要成员,也是Map族(如下图所示)中我们最为常用的一种。简单地说,HashMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即存储的对象是 Entry (同时包含了 Key 和 Value) 。特别地,此外,HashMap 是 Map 的一个非同步的实现。

2、设计思想

HashMap采用的是哈希表的数据结构。哈希表是一种以来存储的数据结构,哈希表的哈希函数通常有 直接定址法、平方取中法、求余法等。
而hashMap的设计者则采取求余法来对其进行元素的均匀分配。
在源码中,有这么一句代码tab[i = (n - 1) & hash],n代表的是数组的长度,hash是指的key的hashCode()值,这句话设计者的意思是想让其相当于hash%n(位运算比%的运算要快,源码中有大量这种位运算),那么你可能会问了,怎养才能使得这两个相等呢?这就涉及到位运算的知识了。
首先要知道1&1=1,0&0=0,1&0=0,0&1=0

要使得 (n-1)&hash <==> hash%n,就得使得n为2的n次幂,因为2的n次幂的二进值形式为0000000100000xxxx,因为hashmap的数组长度初始化为1<<4,也就是16,那么就以16为例子,二进制为0...010000 ,n - 1也就是 0...01111,那么(n - 1)& hash为

hash :              00101010 00011111 00101010 01110100
                                                        &
n - 1:              00000000 00000000 00000000 00001111
---------------------------------------------------------
                    00000000 00000000 00000000 00000100
(可以看出得到的值是在0-15之间(这就保证了==>hash%n), 而且决定最终值的是hash的后面4位)
    这是hash值的计算方式。
     static final int hash(Object key) {
        int h;
        //将前面的16位和后面的16位进行异或,使扰动之后的index结果尽可能的不同
        //这么做也能在保证table的length比较小的时候,也能
        //保证到高低位的bit都参与到hash的计算中来,同时也不会有太大的开销
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
注意,由于hashCode()可以进行重写(hashCode和equals()这两个方法一定要
一起重写),所以key的hash值有可能重复(另外字符的hashCode()也有可能重复,如一些特殊字符)

那么你又会问了HashMap是如何保证长度是2的n次幂的呢?
这就要看tableSizeFor()了,这个方法保证了数组table的长度是不小于cap的最小2的n次幂

static final int MAXIMUM_CAPACITY = 1 << 30;//超过这个,就变成负数了
//得到不小于cap的最小整次幂(最高位1后面都变成1,然后再加1)
//eg:1 2 4 8 16 32
//cap:5 15 30    传入的cap
//n:8 16 32      得到的n
static final int tableSizeFor(int cap) {
    int n = cap - 1;//为了避免cap是2的整次幂,从而导致n变成cap的2倍
    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;
}

00000000 00000000 00000000 00000100  n
00000000 00000000 00000000 00000010  n>>>1   n|n:00000000 00000000 00000000 00000110
00000000 00000000 00000000 00000001  n>>>2   n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000000  n>>>4   n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000000  n>>>8   n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00000000  n>>>16  n|n:00000000 00000000 00000000 00000111
00000000 00000000 00000000 00001000  n+1

2.1初始化和扩容

Q:HashMap是什么时候进行初始化和扩容的,扩容的长度是多少,扩容是怎么进行的。
A:你可能会说,那我判断size>table.length,不就是可以进行扩容了吗?首先table数组并不是等到装满了,再进行扩容的,因为这样子会使得hash的碰撞增多,但是又不能太小就进行扩容,因为这会导致table里面的空元素增多,对table的空间进行浪费,这就使得我们要确定一个阀值对table数组进行扩容,而确定这个阀值的因素,我们就叫做 加载因子 。在hashMap中,加载因子loadFactor默认为0.75f。为什么是这个值,我总结了几点下面几点原因:
1、从区间上来说,0.5太小,会减少hash碰撞,造成空间的浪费,定义为1的话,则会造成hash碰撞增多
2、从数学角度上来说,由于长度是2的整次幂,所以*0.75一般都可以得到一个整数的结果
3、从泊松分布上来说,0.7多可以使得碰撞的元素在8以上的概率极大减小
但是为什么是0.75这个值,其实只是个经验值,和泊松分布并没有什么关系,这是java官方自己说的.

//初始化和扩容
     final Node[] resize() {
        Node[] 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){
                //对旧的数组长度和旧的预期值进行2倍的扩容
                         
                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;//默认的初始化长度 1<<4 16  
            //初始化预期值
           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[] newTab = (Node[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                //对旧的数组进行一个数据迁移
                Node e;
                if ((e = oldTab[j]) != null) {
                    //如果oldTab[j] 里面有东西(红黑树或者链表)
                    oldTab[j] = null;//清除旧的格子,对节点进行重新分布
                    if (e.next == null){//只有一个节点
                        newTab[e.hash & (newCap - 1)] = e;//用哈希函数重新计算index
                    }else if (e instanceof TreeNode){
                        //格子里面装着红黑树,对其进行拆解...
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    }else { // preserve order
                    //格子里面装着多个节点,先初始化4个节点
                    //分别是头尾高低节点
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;//下一个节点
                        do {
                            next = e.next;
                            //oldCap是2的整次幂 (因此二进制是
                            //10000....),以16为例子,结果只可能
                            //是0或者16
                            if ((e.hash & oldCap) == 0) {
                                //代表是在0-16里面
                                //所以新的数组还在里面,就不用迁移
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }else {
                                //节点的hash是可以在16之外的
                                //进行迁移
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            //hash&16判断比较小的数据
                            //位置不变
                            loTail.next = null;
                            newTab[j] = loHead;//loHead存储着hash…&16之后比较小
                            //节点集合
                        }
                        if (hiTail != null) {
                            //hash&16判断比较小的数据
                            //位置加上旧的数组长度
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;//hiHead存储着hash&16之后比较大
                            //节点集合
                        }
                    }
                }
            }
        }
        return newTab;
    }

从上面可以看出,hashMap在进行扩容的时候,会对长度进行原来2倍的扩充,并且对table[index] 元素上面的链表进行迁移,这里注意,因为table[index]元素里面的单链表,有可能会有高的hash值(超过旧的数组长度为高)和低位的hash值,那么我们就要把低位和高位分别存在一个链表集合中,这时候源码是用(e.hash & oldCap) == 0来区分的高低位置的。以16为例,二进制为0...01000,所以&它的值要么是0,要么是16,并且用loHead和hiHead存储起来。所以,扩容之后的index要么是原来的,要么是加上+旧的长度length

3、Hashmap结构图(红黑树后面再讲解)

jdk1.8 HashMap源码解析(上)_第1张图片
图1.png

这图就很好的表现了hashMap的数据结构,当key的hash的值相等的时候,它采取尾接法,形成一条单链表.

4、put过程

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
/**
  @param evict 如果为false,则表处于创建模式。
  如果为true的话,就删除最久没使用的,
  这个是用在LinkedHashMap里面的
  */
  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {

        Node[] tab; Node p; int n, i;
        //判断当前的table是否为空,为空的话初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//(n-1)&hash 想当于hash%n 随机分布到0-15
            tab[i] = newNode(hash, key, value, null);//当前的table[i]里面没有链表,初始化
        else {
            //当前table[i]存在节点
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))){
            //如果要插入的节点已经存在,且key相等,赋值给e
                e = p;
            }else if (p instanceof TreeNode){
                //如果table[i]里面已经有红黑树,则插入红黑树,并进行旋转的操作
                //如果有相同hash和相同key的话,就返回值
                //具体请看下一篇博文红黑树,里面有具体的讲解
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            }else {
                //单链表,往下查找并插入节点  尾插法
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //找到null的节点,进行插入
                        p.next = newNode(hash, key, value, null);
                        //如果这个大于等于8,则把单链表变成红黑树
                        //这里你可能有个疑问,8-1不是等于7吗?
                        //那是因为(binCount是从下标0开始的!!!!)
                        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)))){
                        //往下找,找到相同的key节点,则break结束循环
                        break;
                        }
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
            //e不为null,则说明有相同的节点,则修改value的值
                V oldValue = e.value;
                //由于put操作的时候,onlyIfAbsent传的是false
                //则旧的值终会被替换
                /**@param onlyIfAbsent 代表着是否保留旧的value
                **/
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //afterNodeAccess 是一个回调方法,回调相同key的节点
                /**@link LinkedHashMap有用到这个**/
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//结构修改的次数加1   迭代器 failfast
        //size 加1之后判断是否超出预期的容量值
        if (++size > threshold){
            //进行扩容
            resize();
        }
        afterNodeInsertion(evict);//也是LinkedHashMap用到的.
        //根据Lru算法来删除最久没使用的
        //节点
        return null;
    }
jdk1.8 HashMap源码解析(上)_第2张图片
java8 put流程图.png

从put过程可以看出,判断table是否为空,若为空,则执行resize方法初始化table数组,如果table[i = (n-1)&hash]的值为空,则初始化一个链表节点,如果不为null,则判断头节点是否和要put的节点key相等,如果不等,就判断链表是否被红黑树化,需要搜索红黑树查找该key值是否已存在;链表未被树化,且头节点key值与新增key值不同,需要遍历链表查找该key值是否存在。如果不存在,则插入,这里需要注意的是,当链表>=8的时候,则转化成红黑树
上面的判断逻辑中,不管是哪一种,只要key值已存在,则赋值给e,因此在后面判断e是否为null,若不为空,则表示已存在,根据策略决定是否覆盖原值
如果key值不存在,需要将modCount自增1,然后将size自增,判断大小是否超过阈值,是,则通过resize方法double原table数组的容量。

你可能感兴趣的:(jdk1.8 HashMap源码解析(上))