HashMap源码分析

1.8之前的HashMap存储数据使用了数组+链表方式
1.8之后的HashMap存储数据使用了数组+链表+红黑树的方式

声明:本文基于jdk1.8分析

HashMap源码分析_第1张图片
首先来看其构造函数:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

构造了初始容量16,加载因子0.75的一个空的HashMap。

接着我们调用put方法第一次往HashMap中存放数据

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

这里先调用了hash方法计算hash值

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

这是一个扰动函数,将key的hashCode和该hashCode的高16为进行异或计算出hash。后面会通过hash和数组长度-1进行&运算,来计算出当前要put的数据放入数组哪个桶中。比如当前我们初始容量16,也就是2的4次方。(16-1)的二进制刚好低4位都是1( 0000 0000 0000 1111),所以hash&(16-1)刚好就是取低4位。容量为32也就是2的5次方,低5位都是1。前面进行扰动函数就是为了让计算后的hash具有hashCode高位和低位的特征,降低hash碰撞的概率。

知道了hash方法的计算原理后,继续看putVal方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
   //这些局部变量会在下面的if语句中赋值
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
      //注释1:如果table是空的,就初始化数组
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
      //注释2:要放在数组中哪个位置,且该位置当前是空的
        tab[i] = newNode(hash, key, value, null);
    else {
       //注释3
       //...这个逻辑先不看
    }
    ++modCount;
  	//如果hashmap中的节点大于threshold就扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

先看注释1代码:因为我们是第一次put,所以tab是空的,此时需要先初始化数组,也就是调用resize()方法

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //初始化时候oldCap=null,oldThr=0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        //..先不看
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
       //..先不看
    else {               // zero initial threshold signifies using defaults
      	//初始化newCap=16 newThr = 12(16*0.75)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
   //创建node数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
   //将node数组赋值给成员变量table
    table = newTab;
    if (oldTab != null) {
      //...先不看
      //初始化不会走这个逻辑,该逻辑用来处理扩容后,将旧数组中的所有数据迁移到新数组。
    }     
    return newTab;
}

在看注释2代码:首先通过(n - 1) & hash计算出要放在数组中哪个位置,如果该位置当前是空的,那么就创建一个新的Node,,并放入数组对应桶中。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

这样我们第一个数据就被插入HashMap中了。接着当我们继续插入数据。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    //这些局部变量会在下面的if语句中赋值
    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)
      	//如果数组中位置i对应的桶没有数据,就把要插入的数据放入该桶中
        tab[i] = newNode(hash, key, value, null);
    else {
      	//如果数组位置i的桶不是空的,也就是有node了,那我们新的数据就以节点的方式插入链表尾部或者红黑树中
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
          	//如果要插入的数据的key和数组中元素的key相同,那就把它赋值给变量e
            e = p;
        else if (p instanceof TreeNode)
          	//如果数组中的节点是TreeNode类型的,说明数据结构已经转为红黑树了,就调用TreeNode的putTreeVal方法插入
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
          	//如果是链表形式,就从链表头部开始,也就是数组中的那个,不停往后遍历
            for (int binCount = 0; ; ++binCount) {
              	//如果找到链表最后一个节点,并且没有找到和要插入数据key相同的,就创建一个新节点,插入链表尾部(尾插)
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                  	//如果链表中节点树大于等于TREEIFY_THRESHOLD,链表会进行树化,链表-->红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
              //如果在遍历过程中找到了某个节点和要插入数据key相同,就跳出循环 次数变量e就是该节点。
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
      //如果e不为空,就说明新数据的key在hashmap中存在相同的,此时直接用新value替换旧的value就好了。
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
          //返回旧value
            return oldValue;
        }
    }
    ++modCount;
  //如果大于阈值,就扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

在我们往HashMap插入数据时,首先会通过(n - 1) & hash计算出要插入在数组中的位置,如果该位置还没数据,就直接放入。如果已经有了,那就遍历数组该桶中所有的节点,它们以链表或者红黑树的方式存储。如果在链表中没有找到就创建新节点插入链表尾部,如果找到有相同key的节点,那就替换掉value。插入时如果链表长度大于TREEIFY_THRESHOLD此时链表会转为红黑树。

关于红黑树处理会涉及它的自平衡,变色和旋转,本篇不会做讲解,感兴趣的可以自己去了解红黑树相关知识。

每次插入数据后都会判断HashMap的size是否大于阈值,如果大于就会调用resize()方法进行扩容处理,注意size是HashMap中总节点数量,扩容是扩容我们数组的长度。

之前我们了解了resize()中初始化逻辑,现在来看看它是如何进行扩容的吧

final Node<K,V>[] resize() {
  	//旧的节点数组和数组长度
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
  	//新的节点数组和数组长度
    int newCap, newThr = 0;
  	//如果旧的数组长度大于0,此时我们进行扩容
    if (oldCap > 0) {
      	//如果容量已经达到最大值了,不能进行扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
       //在旧数组容量基础上扩容两倍,比如16-->32
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
          //新的阈值时原来的两倍,比如12-->24
            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;
  	//下面代码是将旧数组中的所有节点迁移到新数组中,需要重新根据hash&(newCap - 1)计算放入的数组下标
    if (oldTab != null) {
      	//遍历旧数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
          	//如果数组中的元素不为空,就赋值给e
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
               //数组中该位置只有一个节点,直接根据hash和新数组长度计算到要放入的新数组中的下标,直接放入即可
                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
                  	//处理节点是链表的情况
                  	//loHead低位头节点,loTail低位尾节点
                    Node<K,V> loHead = null, loTail = null;
                  	//hiHead高位头节点,hiTail高位尾节点
                  	//低位代表新数据下标为i的位置,高位代表新数组下标为j + oldCap的位置
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                      	//不停的遍历链表中的节点
                      	//对于节点hash&oldCap == 0的情况,组织成链表,头节点是loHead 
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                      	//对于节点hash&oldCap != 0的情况,组织成链表,头节点是hiHead
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                  //把loHead放入新数组j位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                  //把hiHead放入新数组j + oldCap位置
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

上面的代码中e.hash & oldCap 是否为 0来判断该节点是放在新数组的低位和高位逻辑看起来挺唬人的,其实就是对e.hash & (newCap - 1)的优化,效果是一样的。我们可以做个测试:

//测试代码
    public static void main(String[] args) {
        int hash = 1111;
        int oldLength = 32;
        int oldIndex= hash & (oldLength - 1);
        System.out.println("oldIndex = "+oldIndex);

        System.out.println("通过hash & oldLength来计算在新数组中的位置");
        int flag = hash & oldLength;
        if (flag == 0){
            //低位
            System.out.println("应该放在新数组中的位置(低位):"+oldIndex);
        }else {
            //高位
            System.out.println("应该放在新数组中的位置(高位):"+(oldIndex+oldLength));
        }

        System.out.println("\n");

        System.out.println("通过hash & (newLength - 1)来计算在新数组中的位置");
        int newLength = 64;
        int index2= hash & (newLength - 1);
        System.out.println("应该放在新数组中的位置:"+index2);
    }

打印结果:

oldIndex = 23
通过hash & oldLength来计算在新数组中的位置
应该放在新数组中的位置(低位):23

通过hash & (newLength - 1)来计算在新数组中的位置
应该放在新数组中的位置:23

修改hash 为 2222,打印结果如下:

oldIndex = 14
通过hash & oldLength来计算在新数组中的位置
应该放在新数组中的位置(高位):46

通过hash & (newLength - 1)来计算在新数组中的位置
应该放在新数组中的位置:46

多次测试可以发现hash & (newLength - 1)的结果总是和oldIndex或者(oldIndex+oldLength)相等。

到这里关于HashMap主要的流程就分析完成了,这里做个总结:

  • 1.8之后的HashMap存储数据使用了数组+链表+红黑树的方式
  • 通过扰动函数和扩容来降低hash碰撞的概率
  • 通过链表法解决hash碰撞
  • 扩容的条件是hashmap.size>=capacity*loadFactor
  • 每次扩容后长度是原数组长度的2倍,扩容操作需要把oldTable 的所有键值对重新插入newTable 中
  • 当⼀个桶存储的链表⻓度达到 8 时会将链表转换为红⿊树。

你可能感兴趣的:(JAVA基础,算法,hash,java)