Java集合源码解析:HashMap

本文概要

  1. HashMap概述
  2. HashMap数据结构
  3. HashMap的源码解析

HashMap概述

在官方文档中是这样描述的:

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

我们可以总结一下:

  1. 基于Map实现,也就是key-value形式去存储
  2. 允许key为null,允许value为null
  3. 非同步,它不是线程安全的
  4. 没有顺序

HashMap数据结构

JDK7以及之前HashMap使用的是数组+链表
Java集合源码解析:HashMap_第1张图片

JDK8以后HashMap使用的是数组+链表+红黑树(我们这篇文章主要讲的是JDK8)

链表大于一定长度会转换为红黑树,主要是为了提高操作效率

Java集合源码解析:HashMap_第2张图片

HashMap的源码解析

源码解析主要为以下几个方面去分析

  1. HashMap主要的成员变量
  2. HashMap的构造函数
  3. get()方法
  4. put()方法
  5. resize()扩容方法

HashMap主要的成员变量

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
 	/* ---------------- Fields -------------- */
	// 默认数组初始化容量
    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;

	// 桶中结构转化为红黑树对应的数组的最小长度,如果当前数组的长度(即table的长度)小于它,就不会将链表转化为红黑树,而是用resize()代替
    static final int MIN_TREEIFY_CAPACITY = 64;

    // 存储元素的数组
    transient Node<K,V>[] table;

     // 存储元素的集
    transient Set<Map.Entry<K,V>> entrySet;

     // 存放元素的总个数
    transient int size;

     // 更改结构的计数器(比如put()、remove()等对hashmap结构有改动的操作,那么该数值都会+1)
    transient int modCount;

    // 扩容临界值,当size > threshold。就会进入扩容
    int threshold;
    
     // 扩容因子
    final float loadFactor;
}

HashMap构造函数

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    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);
    }

这里总结两点

  1. 创建HashMap对象,调用构造函数时,没有初始化table数组
  2. 如果调用了第三个构造函数(即传入了initialCapacity初始化容量和扩容因子),会调用tableSizeFor(),虽然这时候会把值赋给扩容临界值threshold,但是第一次put(),会进行resize(),然后初始化table数组,这时候会把threshold当成table数组的长度,所以暂时我们可以理解这个threshold就是容量,但是实际上它还是扩容临界值,只不过第一次比较特殊。这里会把initialCapacity转换成大于initialCapacity的最靠近2次幂的那个数,比如说initialCapacity = 10,经过tableSizeFor(10)后,threshold = 16。因为16是2次幂的数,也是最靠近10的。

这里需要说下tableSizeFor(initialCapacity)方法

	static final int tableSizeFor(int cap) {
        int n = cap - 1;
        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;
    }

下面用一个图解释下tableSizeFor(10) :
Java集合源码解析:HashMap_第3张图片

get()方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    // 对key进行hash
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这里没有直接使用key的hashCode,而是使key的hashCode高16位不变,低16位与高16位异或作为最终hash值。
原因就是:如果直接使用key的hashCode作为hash很容易发生碰撞。比如n-1为15(0x000f)时,散列值真正生效的只是低4位,当新增的值hashCode为2、18、34这些以16位倍数的等差数列,就产生大量碰撞

    // get()实际上就是调用getNode()
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        /*	   
	     * 1、首先判断table是否为空、table的长度是否大于0
	     * 2、hash & (n - 1),取的这个hash在这个数组的下标,类似于(hash % (n-1)),但是&效率更高
	     * 3、tab[hash & (n - 1)],获取该数组在该索引的的头元素,
	     */
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            /*
             * 判断该key是否为头元素
             */             
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
             // 如果不相等,然后获取头元素的下一个元素
            if ((e = first.next) != null) {
           		// 判断头元素是否为红黑树节点
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 不是红黑树节点,那么就是链表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

整个get()方法,还是比较简单,可以总结为几点

  • hash = hash(key),获取key的hashcode,用于获取在数组中的下标
  • (n - 1) & hash,通过hashcode与数组的长度进行&运算,获取该hashcode在数组中的下标。
  • first = tab[(n - 1) & hash],获取该下标的头元素
  • first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))),判断该key是否为头元素
  • 因为可能会出现hash碰撞【即为不同的hashcode可能定位到同一个下标】,所以判断该头元素是为红黑树节点,还是链表,然后在节点中进行循环判断。

put()方法

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        /*
         * 我们说过创建HashMap对象,是不初始化table数组的。
         * 所以第一次调用put()的时候。table数组为空。
        */ 
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 那么调用resize()进行初始化table数组
            n = (tab = resize()).length;
        //  通过hash定位在table数组下的索引,判断该索引是否存在元素
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 不存在元素,直接往该索引下插入一个Node元素
            tab[i] = newNode(hash, key, value, null);
        else {
        	// 说明该索引下存在头元素
            Node<K,V> e; K k;
            // 判断插入的key与头元素的key是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 说明插入的key与头元素的key相等,赋值给e变量
                e = p;
            else if (p instanceof TreeNode)
            	// 说明头元素是红黑树节点
            	// 判断树中是否存在一个节点的key与插入的key相等,存在赋值给e,不存在,往红黑树节点插入节点,并且返回null
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	// 说明头元素是链表中的一个节点
            	// 循环遍历该链表
                for (int binCount = 0; ; ++binCount) {               	
                    if ((e = p.next) == null) {
                    	// 如果直到链表的尾节点,都没有找到与该key相等的节点
                    	// 往该链表插入一个新的节点
                        p.next = newNode(hash, key, value, null);
                        // 判断该链表的长度是否大于等于TREEIFY_THRESHOLD
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	// 调用treeifyBin(),判断是否需要把链表转为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }                    
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 找到了和该key相等的节点e,直接break跳出循环
                        break;
                    p = e;
                }
            }
            // 如果e不为空,说明该链表或者红黑树中存在与该key相等的节点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                	// 把该节点的值替换成新的值
                    e.value = value;
                // 回调方法,HashMap没有实现,忽略
                afterNodeAccess(e);
                // 返回旧的值
                return oldValue;
            }
        }        
        ++modCount;
        // size+1,并且判断大小是否大于扩容阈值
        if (++size > threshold)
        	// 进行扩容
            resize();
        // 回调方法,HashMap没有实现,忽略
        afterNodeInsertion(evict);
        return null;
    }

我们看下TreeNode.putTreeVa(),往红黑树里添加节点

	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<K,V> root = (parent != null) ? root() : this;
            // 遍历树
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                // 如果p的hash大于传入的hash
                if ((ph = p.hash) > h)
                	// 把-1赋值给dir,代表左边查找树
                    dir = -1;
                // 如果p的hash小于传入的hash
                else if (ph < h)
                	// 把-1传递给dir,代表右边查找树
                    dir = 1;
                // 如果传入的hash和p.hash相等,而且p.key 等于传进来的key,那么直接返回p
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                // 如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    // 如果k所属的类没有实现Comparable接口 或者 k和p节点的key相等
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        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;
                    }
                     // 从p节点的左节点和右节点分别调用find方法进行查找, 如果查找到目标节点则返回
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                // 根据dir的值,获取p的左节点或者右节点,判断获取的节点是否为空
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                	// 如果获取的节点为空,那么则需要往树里插入一个新节点
                    Node<K,V> xpn = xp.next;
                    // 创建一个新Node节点
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    // 如果dir小于0
                    if (dir <= 0)
                    	// 插入左节点
                        xp.left = x;
                    else
                    	// 如果dir大于0
                    	// 插入右节点
                        xp.right = x;
                    // 这里进行调整指针
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    // 插入新节点后可能会破坏红黑树结构,所以需要调用balanceInsertion(root, x)进行修复红黑树结构
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

转发几篇文章讲tieBreakOrder()方法分析、红黑树修复调整

treeifyBin(),判断是否需要把链表转为红黑树

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // MIN_TREEIFY_CAPACITY = 64
        // 判断table的长度是否小于MIN_TREEIFY_CAPACITY 
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
       		// 调用 resize()进行扩容
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {        	
            TreeNode<K,V> hd = null, tl = null;
            // 创建一条以TreeNode为节点的链表,方便以后红黑树转为链表
            do {                        	
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
            	// 把TreeNode的链表转为红黑树
                hd.treeify(tab);
        }
    }

treeify(),把TreeNode的链表转为红黑树,原理很简单,就不解释,不懂的可以看这篇文章讲红黑树的

        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

resieze()方法

resize()作用主要是初始化table数组、对table数组进行扩容

    final Node<K,V>[] resize() {
    	// 把旧table数组赋值给oldTab
        Node<K,V>[] oldTab = table;
        // 获取旧table数组的长度,赋值给oldCap 
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 获取旧的扩容阈值,赋值给oldThr 
        int oldThr = threshold;
        // 定义两个变量,newCap存储新的数组长度,newThr存储新的扩容阈值
        int newCap, newThr = 0;
        // 如果旧table数组长度>0
        /*
         * 什么时候会出现这种情况?
         * PS1:table数组已经存在,需要进行扩容
         */
        if (oldCap > 0) {        	
        	// 如果旧table数组长度>=最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {          	
            	// 把Integer.MAX_VALUE赋值给扩容阈值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
             // 如果旧table数组长度<最大容量 
             // 把oldCap<<1(相当于oldCap*2)赋值给newCap作为新的table容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 旧的扩容阈值oldThr<<1(相当于oldThr*2),赋值给newThr作为新的扩容阈值
                newThr = oldThr << 1; // double threshold
        }
        // 如果旧table长度 == 0 而且 旧扩容阈值>0
        /*
         * 什么时候会出现这种情况?
         * PS2:当创建new HashMap(16,0.75),赋值了扩容阈值和扩容因子,而且第一次调用put(key,value)的时候,
         * 上面分析put()方法时候讲过,第一次调用put(key,value)的时候,会调用resize()方法进行初始化table组。
         */
        else if (oldThr > 0) // initial capacity was placed in threshold
        	// 把旧的扩容阈值赋值给newCap 作为新的table容量
            newCap = oldThr;
        // 如果旧table长度 == 0 而且 旧扩容阈值 == 0
         /*
         * 什么时候会出现这种情况?
         * PS3:当创建new HashMap(),而且第一次调用put(key,value)的时候
         */
        else {               // zero initial threshold signifies using defaults
        	// 把默认初始化容量(16) 赋值给newCap作为新的table容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 默认扩容因子(0.75)*默认初始化容量(16)赋值给newThr作为新的阈值(12)
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 如果新的阈值==0
         /*
         * 什么时候会出现这种情况?
         * 当遇到上面PS2的时候
         */
        if (newThr == 0) {  
        	// 新的容量 * 扩容因子 赋值给ft    	
            float ft = (float)newCap * loadFactor;
            // 如果(新容量 < MAXIMUM_CAPACITY 而且ft < MAXIMUM_CAPACITY),那么ft赋值给newThr 作为新的阈值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 把新的阈值赋值给成员变量threshold作为当前的阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 创建newCap长度的数组,作为扩容后的table数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 把newTab执行成员变量table数组 
        table = newTab;
        // 如果旧table数组不为空,那么需要把旧table的元素转移到newTab 
        if (oldTab != null) {
			// 遍历旧table数组        
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 获取j下标的元素赋值e,并且不为空
                if ((e = oldTab[j]) != null) {
                	// 把j下标的元素置为null,利于GC
                    oldTab[j] = null;
                    // 如果e的下一个元素为空
                    if (e.next == null)
                    	// 通过(e.hash & (newCap - 1))获取e元素在新数组的下标,并且把e指向该数组
                        newTab[e.hash & (newCap - 1)] = e;
                   // 如果e存在下一个元素,而且e为红黑树节点
                    else if (e instanceof TreeNode)                    	
                    	// 那么调用e的TreeNode.split()方法进行迁移,这个后面在说
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 如果e存在下一个元素,而且e是一个链表
                    else { // preserve order                  	
                    	/*
	                   	 * 这里通俗一点,我把loHead、loTail称为不需要迁移的头指针和尾指针
	                   	 * 				把hiHead、hiTail称为需要迁移的头指针和尾指针
	                   	 */
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 开始遍历e的链表
                        do {            
                        	// 获取e的下一个节点,赋值给next            
                            next = e.next;
                            // 通过e.hash & oldCap == 0 判断元素是否需要迁移,这个后面在讲,为什么这样算
                            if ((e.hash & oldCap) == 0) {
                            // 等于0,那么元素是不需要迁移的。在oldTab数组里的下标等于newTab数组里的下标
                            // 那么这里使用指针loHead、loTail来构造不需要迁移的链表
                            	// 如果 loTail  == null                        	
                                if (loTail == null)
                                	// 这里只会第一次(e.hash & oldCap) = 0的时候,才会进来
                                	// 把e指向loHead 
                                    loHead = e;
                                else
                                	// 把loTail.next指向e,构造一条不用迁移的链表
                                    loTail.next = e;
                                // 把e指向loTail
                                loTail = e;
                            }
                            else {
                            	// 不等于0,那么元素是需要迁移,那么使用另一个指针hiHead、hiTail来存构造需要迁移的链表
                                if (hiTail == null)
                                	// 这里只会第一次(e.hash & oldCap) != 0的时候,才会进来
                                	// 把e指向hiHead 
                                    hiHead = e;
                                else
                                	// 把hiTail.next指向e,构造一条需要迁移的链表
                                    hiTail.next = e;
                                // 把e指向hiTail 
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 如果不需要迁移链表的尾指针loTail不为空
                        if (loTail != null) {
                            loTail.next = null;
                            // 直接把不需要迁移链表的头指针指向新table数组的j索引
                            newTab[j] = loHead;
                        }
                        // 如果需要迁移链表的尾指针hiTail不为空
                        if (hiTail != null) {
                            hiTail.next = null;
                              // 直接把不需要迁移链表的头指针指向新table数组的(j+旧table长度)索引下
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

我们这里重点总结下链表的迁移

  • 先创建一个不需要迁移链表、需要迁移链表
  • 通过节点的hash值与旧数组table的长度做&是否等于0,判断元素是否迁移
  • 把不需要迁移的链表直接放到新table的j索引下,把需要迁移的链表放到(j+oldCap)索引下

为什么使用e.hash & oldCap == 0 判断是否需要迁移,而且直接把需要迁移的链表放到(j+oldCap)索引下?

回忆一下之前我们定位一个元素在数组那个索引下,是通过(e.hash & (oldCap-1))去获取索引,举个例子一个元素的hash为24,table的长度为16,那么它通过(e.hash & (oldCap-1))获取的索引为8,如下图:
Java集合源码解析:HashMap_第4张图片

OK,这时候需要调用resize()进行扩容,那么table的长度变为32,那么该元素的就需要判断是否需要迁移,通过(e.hash & oldCap == 0),如下图
Java集合源码解析:HashMap_第5张图片
很明显得出的值等于16(大于0)是需要迁移,那么该元素在新table的下标是多少?我们在回到上面的公式(e.hash & (newCap-1))去获取该元素在新table的下标,如下图
Java集合源码解析:HashMap_第6张图片

那么得到该元素在新table的下标为24,刚好为(该元素在旧table的索引8 + 旧table的长度16 = 24)。所以我们可以得出结论,对链表的迁移,它在新的Table下的索引只有两个位置,第一个:与在旧table的索引一样,第二个:(该元素在旧table的索引 + 旧table的长度)。

对红黑树的迁移原理差不多,大家可以自行去看代码去研究

不得不说这个设计很巧妙。而且这也是为什么table的长度一定要为2次幂的理由。

你可能感兴趣的:(Java集合源码解析)