JDK1.8中HashMap的实现原理及源码分析

一、概要

             在JDK1.8之前,HashMap采用桶+链表实现,本质就是采用数组+单向链表组合型的数据结构。它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap通过key的hashCode来计算hash值,不同的hash值就存在数组中不同的位置,当多个元素的hash值相同时(所谓hash冲突),就采用链表将它们串联起来(链表解决冲突),放置在该hash值所对应的数组位置上。结构图如下:

    图中,紫色部分代表哈希表,也称为哈希数组,数组中每个元素都是一个单链表的头结点,链表是用来解决冲突的,如果不同的key映射得到了数组的同一位置处,就将其放入单链表。

hash碰撞会给性能带来灾难性的影响,如果多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。

随着HashMap的大小的增长,get()方法的开销也越来越大。由于所有的记录都在同一个桶里的超长链表内,平均查询一条记录就需要遍历一半的列表。

 在JDK1.8中,HashMap的存储结构已经发生变化,它采用数组+链表/红黑树这种组合型数据结构。当hash值发生冲突时,会采用链表或者红黑树解决冲突。使用这样做的结果会更好,是O(logn),而不是糟糕的O(n)。

存储结构有以下几种情况:

  • 当同一hash值的结点数小于8时,则采用链表;
  • 当同一hash值的结点数大于或者等于8并且但是数组长度小于64位,进行扩容操作
  • 当同一hash值的结点数大于或者等于8并且数组长度大于等于64位,则转为红黑树结构。这个重大改变,主要是提高查询速度。它的结构图如下:

  JDK1.8中HashMap的实现原理及源码分析_第1张图片

在这里简单提下数组和链表的区别:

数组

优点:物理地址连续+按下标随机访问效率高O(1)

缺点:插入,删除效率低

链表

优点:存储地址不连续,可灵活的扩展自己的长度,插入,删除效率高

缺点:访问效率低O(n)

而哈希表(Hash类数据结构)正是结合了两者的优点,而衍生出来的的一种高效的数据存储结构,本质上是采用空间换时间的方式,提高了读写的效率。

二、主要源码解析

1、成员变量

    /**
     * 哈希表(Hash table)默认bucket的初始容量16
     * // 00000000 00000000 00000000 00000001 <<4 之后 00000000 00000000 00000000     
     * 00010000 2的4次方
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

    /**
     * bucket最大容量,2的30次方  
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     *默认负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 当容器被添加的元素节点超过8个(可以等于8个)时,容器转化为树形容器
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 节点数小于6个,解散树形结构
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 树形结构的最小表/桶容量
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
/**
     * 哈希桶,存放链表,桶的数量通常使用质数
     */
    transient Node[] table;

    /**
     *HashMap将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
     */
    transient Set> entrySet;

    /**
     *HashMap中实际存在的Node数量,注意这个数量不等于table的长度,甚至可能大于它,因为在table的每个节点上是一个链表(或RBT)结构,可能不止有一个Node元素存在。
     */
    transient int size;

    /**
     *HashMap的数据被修改的次数,这个变量用于迭代过程中的Fail-Fast机制,其存在的意义在于保证发生了线程安全问题时,能及时的发现(操作前备份的count和当前modCount不相等)并抛出异常终止操作。
     */
    transient int modCount;

    /**
     * 哈希表内元素数量的阀值(table.length(哈希桶的个数) * load factor),当哈希表内元素数量超过阀值时,会发生扩容resize().
     */
    int threshold;

    /**
     * 加载因子(填充比),用于计算哈希表元素数量的阀值。
     */
    final float loadFactor;

2、链表节点Node

static class Node implements Map.Entry {
        final int hash;//哈希值
        final K key;//key
        V value;//value
        Node next;//后置节点

        Node(int hash, K key, V value, Node next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        /**
         * 每一个节点的hash值,是将key的hashCode和value的hashCode异或得到的
         * ^:异或,两个操作数的位中,相同则结果为0,不同则结果为1
         * 如:15^2=13
         * 00000000 00000000 00000000 00001111=15
         * 00000000 00000000 00000000 00000010=2
         * 00000000 00000000 00000000 00001101=13
         */
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        /**
         * 设置新的值同时返回旧的值
         */
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry e = (Map.Entry)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

3、构造函数及调用方法

    /**
     * Constructs an empty HashMap with the default initial capacity
     * (16) and the default load factor (0.75).
     * 空参构造函数(使用默认初始table容量16和默认加载因子0.75)
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    /**
     * Constructs an empty HashMap with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     * 指定初始容量的构造函数(默认加载因子0.75)
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    /**
     * 指定初始容量和加载因子的构造函数        
     */
    public HashMap(int initialCapacity, float loadFactor) {
    	/**
    	 * 初始容量不能小于0
    	 */
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        /**
         * 判断初始容量参数值如果大于最大哈希桶容量(即:数组长度),则将该参数值改为最大哈希桶容
         *量值
         */
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        /**
         * 加载因子不能小于等于0并且是浮点值
         */
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        /**
         * 通过初始容量参数值计算HashMap的元素存储阀值
         */
        this.threshold = tableSizeFor(initialCapacity);
    }
    /**
     * 保证指定的设置的table数组的长度必须是2的n次方,如果你传入的不是2的n次方,
     * 那么经过这个方法出来的值一般都是大于你传入的参数最接近的2的n次方
     */
    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;
    }

    /**
     * 传入Map类的参数的构造函数,将参数map里的所有元素加入HashMap表中
     */
    public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
final void putMapEntries(Map m, boolean evict) {
    	//获取要插入map的大小
        int s = m.size();
        if (s > 0) {
            if (table == null) { // 当前表为空
            	//根据m的元素数量和当前表的加载因子,计算出阈值
                float ft = ((float)s / loadFactor) + 1.0F;
              //修正阈值的边界 不能超过MAXIMUM_CAPACITY
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果新的阈值大于当前阈值
                if (t > threshold)
                	//返回一个 >=t的 满足2的n次方的阈值
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)//如果当前元素表不是空的,但是 m的元素数量大于阈值,说明一定要扩容。
                resize();
            //遍历 m 依次将元素加入当前表中。
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }
 /**
     * 初始化或者加倍表的容量,如果当前表的容量为null,那么根据当前阀值初始化容量,否则,
     * 使用二次幂扩展法,在新表中,每个哈希桶的元素要么保持相同的索引,要么以两个偏移的幂移动
     */
    final Node[] resize() {
    	//oldTab为当前表的哈希桶
        Node[] oldTab = table;
        //当前哈希桶的容量(长度)
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //当前的阀值
        int oldThr = threshold;
        //定义新的哈希桶的长度、新的阀值
        int newCap, newThr = 0;
        if (oldCap > 0) {//如果当前哈希桶的容器(长度)大于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; // 变为原来的两倍,向左位移1位,如:00000001向左位移1位变成00000010
        }
        else if (oldThr > 0) // 如果当前表是空的,但是有阀值
            newCap = oldThr;//那么新表的容量就等于旧的阀值
        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[] newTab = (Node[])new Node[newCap];
        //更新哈希桶引用
        table = newTab;
      //如果以前的哈希桶中有元素
        //下面开始将当前哈希桶中的所有节点转移到新的哈希桶中
        if (oldTab != null) {
        	//遍历老的哈希桶
            for (int j = 0; j < oldCap; ++j) {
            	 //取出当前的节点 e
                Node e;
                //如果当前桶中有元素,则将链表赋值给e
                if ((e = oldTab[j]) != null) {
                	//将原哈希桶置空以便GC
                    oldTab[j] = null;
                	  //如果当前链表中就一个元素,(没有发生哈希碰撞)
                    if (e.next == null)
                    	//直接将这个元素放置在新的哈希桶里。
                        //注意这里取下标 是用 哈希值 与 桶的长度-1 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { 
                    	/**如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
                    	 * 因为扩容使容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,
                    	 * 即high位。
                    	 */
                    	//低位链表的头结点、尾节点
                    	Node loHead = null, loTail = null;
                    	//高位链表的头节点、尾节点
                    	Node hiHead = null, hiTail = null;
                        Node next;//临时节点 存放e的下一个节点
                        do {
                            next = e.next;
                           /**
                            * table的大小一直是2的倍数,2的N次方,因此当前元素插入table的索引的值为其hash值的后N位组成的值,
                            * 那么扩容后的table大小即为2的N+1次方,则其中元素的table索引为其hash值的后N+1位确定,比原来多了一位
                            * 因此,table中的元素只有两种情况:
                            * 1.元素hash值第N+1位为0:不需要进行位置调整
                            * 2.元素hash值第N+1位为1:调整至原索引的两倍位置
                            */
                            	// 判断即用于确定元素hash值第N+1位是否为0,若为0,则使用loHead与loTail,将元素移至新table的原索引处
                            if ((e.hash & oldCap) == 0) {
                            	//给头尾节点指针赋值
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处
                                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;
    }

putVal方法移到"主要方法"这节讲

4、主要方法

(1)增加、修改数据(Put方法调用链)

              

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * HashMap中索引的生成,可参考
     * http://www.importnew.com/16599.html,
     *其中使用了扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因
     *此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。
     */
    static final int hash(Object key) {
        int h;
        /**
         * public native int hashCode()
         * hashCode是原生方法,由C/C++编写的。
         */
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
 /**
     * 如果onlyIfAbsent为true时,相同key的元素,只有原来元素的value不为null时,新put的元素才会对value进行覆盖
     * 如果evict为false,那么HashMap为创建模式
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    	//定义变量tab是将要操作的Node数组引用,p表示tab上的某Node节点,n为tab的长度,i为tab的下标。
        Node[] tab; 
        Node p; 
        int n, i;
       //判断当table为null或者tab的长度为0时,即table尚未初始化,此时通过resize()方法得到初始化的table。                  
        if ((tab = table) == null || (n = tab.length) == 0)//如果当前表为空
            n = (tab = resize()).length;//扩容后的长度(//这种情况是可能发生的,HashMap的注释中提到:The table, initialized on first use, and resized as necessary。)
      //此处通过(n - 1) & hash 计算出的值作为tab的下标i,并另p表示tab[i],也就是该链表第一个节点的位置。并判断p是否为null。
        if ((p = tab[i = (n - 1) & hash]) == null)//如果table的在(n-1)&hash的值是空,就新建一个节点插入在该位置(说明该位置插入前没有元素,也就没有碰撞)
            tab[i] = newNode(hash, key, value, null);//插入元素
        else {//下面进入p不为null的情况,有三种情况:p为链表节点;p为红黑树节点;p是链表节点但长度为临界长度TREEIFY_THRESHOLD,再插入任何元素就要变成红黑树了。
            Node e; K k;//定义e引用即将插入的Node节点,并且下文可以看出 k = p.key。
          /*
           * HashMap中判断key相同的条件是key的hash相同,并且符合equals方法。这里判断了p.key是否和插入的key相等,如果相等,则将p的引用赋给e;
           * 这一步的判断其实是属于一种特殊情况,即HashMap中已经存在了key,于是插入操作就不需要了,只要把原来的value覆盖就可以了。
           * //这里为什么要把p赋值给e,而不是直接覆盖原值呢?答案很简单,现在我们只判断了第一个节点,后面还可能出现key相同,所以需要在最后一并处理。
           */
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            /**
             * p是红黑树节点,那么肯定插入后仍然是红黑树节点,所以我们直接强制转型p后调用TreeNode.putTreeVal方法,返回的引用赋给e。
             */
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {//p为链表节点的情形,也就是上述说的另外两类情况:插入后还是链表/插入后转红黑树。另外,上行转型代码也说明了TreeNode是Node的一个子类。
                for (int binCount = 0; ; ++binCount) {//我们需要一个计数器来计算当前链表的元素个数,并遍历链表,binCount就是这个计数器。
                    if ((e = p.next) == null) {//遍历过程中当发现p.next为null时,说明链表到头了,直接在p的后面插入新的链表节点,即把新节点的引用赋给p.next,插入操作就完成了。注意此时e赋给p。
                        p.next = newNode(hash, key, value, null);//最后一个参数为新节点的next,这里传入null,保证了新节点继续为该链表的末端。
                        /**
                         *如果冲突的节点数已经达到8个,看是否需要改变冲突节点的存储结构,             
            		 *treeifyBin首先判断当前hashMap的长度,如果不足64,只进行
                         *resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树
                         */
                        if (binCount >= TREEIFY_THRESHOLD - 1) //插入成功后,要判断是否需要转换为红黑树,因为插入后链表长度加1,而binCount并不包含新节点,所以判断时要将临界阈值减1。
                            treeifyBin(tab, hash);//当新长度满足转换条件时,调用treeifyBin方法,将该链表转换为红黑树
                        break; //当然如果不满足转换条件,那么插入数据后结构也无需变动,所有插入操作也到此结束了,break退出即可。
                    }
                    /**
                     * //在遍历链表的过程中,我之前提到了,有可能遍历到与插入的key相同的节点,此时只要将这个节点引用赋值给e,最后通过e去把新的value覆盖掉就可以了。
                     */
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { //针对已经存在key的情况做处理
                V oldValue = e.value;//定义oldValue,即原存在的节点e的value值。
                /**
                 * //前面提到,onlyIfAbsent表示存在key相同时不做覆盖处理,这里作为判断条件,可以看出当onlyIfAbsent为false或者oldValue为null时,进行覆盖操作。
                 */
                if (!onlyIfAbsent || oldValue == null)//
                    e.value = value;//覆盖操作,将原节点e上的value设置为插入的新value。
                afterNodeAccess(e);//这个函数在hashmap中没有任何操作,是个空函数,他存在主要是为了linkedHashMap的一些后续处理工作。
                return oldValue;//返回原来的value
            }
        }
        ++modCount;//收尾工作,值得一提的是,对key相同而覆盖oldValue的情况,在前面已经return,不会执行这里,所以那一类情况不算数据结构变化,并不改变modCount值。
        if (++size > threshold)//同理,覆盖oldValue时显然没有新元素添加,除此之外都新增了一个元素,这里++size并与threshold判断是否达到了扩容标准。
            resize();
        /**
         * 这里与前面的afterNodeAccess同理,是用于linkedHashMap的尾部操作,HashMap中并无实际意义
         */
        afterNodeInsertion(evict);
        return null;//最终,对于真正进行插入元素的情况,put函数一律返回null。
    }

(2)获取数据(get方法)

               如果对上面说的都理解的话,下面说的就小菜一碟了。其中调用的方法上面都有详细解释

              因为查询过程不涉及到HashMap的结构变动,所以get方法的源码显得很简洁。核心逻辑就是通过hash值计算出对应的桶位置,然后遍历该位置上的所有节点,分别与key进行比较看是否相等。

public V get(Object key) {
        Node e;
      //根据key及其hash值查询node节点,如果存在,则返回该节点的value值。
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * 根据key搜索节点的方法。记住判断key相等的条件:hash值相同 并且 符合equals方法。
     */
    final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
      //根据输入的hash值,可以直接计算出对应的下标(n - 1)& hash,缩小查询范围,如果存在结果,则必定在table的这个位置上。
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // //判断第一个存在的节点的key是否和查询的key相等。如果相等,直接返回该节点
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {//遍历该链表/红黑树直到next为null。
                if (first instanceof TreeNode)//当这个table节点上存储的是红黑树结构时,在根节点first上调用getTreeNode方法,在内部遍历红黑树节点,查看是否有匹配的TreeNode。
                    return ((TreeNode)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&//当这个table节点上存储的是链表结构时,用同样的方式去判断key是否相同。
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null); //如果key不同,一直遍历下去直到链表尽头,e.next == null。
            }
        }
        return null;
    }

(3)判断是否包含该key

public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

(4)判断是否包含该value

 public boolean containsValue(Object value) {
        Node[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

 

 

你可能感兴趣的:(集合类)