美文网首页
Java-从源码理解HashMap

Java-从源码理解HashMap

作者: 孟校长 | 来源:发表于2019-10-10 21:14 被阅读0次

    前言

    关于HashMap,是Java程序员和Android程序员日常使用频率相当高的一种集合类型了,熟悉的掌握它的使用方式还是很有必要的,要是能做到知其所以然那就更好了。本文参照JDK1.8的源码进行讲解。

    1.介绍

    关于HashMap,它是一种通过键值对映射来存储对象的集合,继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口,它的内部实现原理所包含的知识点还是非常的多的。要想更好的理解HashMap,首先还要对其内部的几个变量的含义有一定的了解。

    1.size: 集合的大小。
    2.loadFactor: 负载因子,默认为0.75(对空间和时间效率的一种平衡值,最好不要自己修改)。
    3.threshold: 临界值,当HashMap的大小达到临界值时,就需要重新分配大小。
    4.table: 存储键值对数据的哈希桶数组(即HashMap中的数据实际是存储在此数组中的)。
    5.Node<K, V>: table中每一个元素的实体类,由hash值,key值,value值,以及next(Node类型)元素组成;
                  Node实现了Map中的Entry接口,其本身是一个单向链表。
    6.TreeNode<K, V>: 红黑树节点实现类,继承自LinkedHashMap.Entry<K,V>类
    

    另外,在JDK1.8以后,HashMap对底层的实现以及扩容机制等进行了优化,采用数组+链表/红黑树的方式存储数据。因为即使负载因子的值和Hash算法设计的再合理也是不会百分之百的避免哈希碰撞的发生,而这样就有可能造成某个位桶下的链表长度过长,当链表的长度超过8的时候,这个链表就将转换成红黑树,因为如果链表的长度过长,就会严重降低HashMap的数据访问速度,而转化成红黑树就是利用了红黑树快速增删改查的特点来提高HashMap的性能。具体结构如下:

    hashbody.png

    有了一些基本的了解之后,我们正式学习下它的源码。

    2.构造方法

    1.无参构造,通常我们都用这个构造方法创建一个HashMap,该构造方法初始化默认的负载因子值为0.75。

    public HashMap() {
        this.loadFactor = 0.75F;
    }
    

    2.指定“容量大小”和“负载因子”的构造函数:

    public HashMap(int var1, float var2) {
        // 如果指定的容量值小于0,则抛出异常
        if (var1 < 0) {
            throw new IllegalArgumentException("Illegal initial capacity: " + var1);
        } else {
            // 如果指定的容量大小超过了1073741824,那么就让容量值为1073741824
            if (var1 > 1073741824) {
                var1 = 1073741824;
            }
            /**
             * 如果指定的负载因子值大于0并且是一个合法数字时,将指定的值赋值给loadFactor变量,
             * 同时根据指定的容量大小来计算临界值的大小,否则抛出异常。
             */
            if (var2 > 0.0F && !Float.isNaN(var2)) {
                this.loadFactor = var2;
                this.threshold = tableSizeFor(var1); // 此方法为计算临界值的方法,详解见下
            } else {
                throw new IllegalArgumentException("Illegal load factor: " + var2);
            }
        }
    }
    
    /**
     * 此方法为计算当前HashMap中临界值的方法,方法中涉及到了按位或操作以及逻辑运算符操作,这里面运算符的使用方式还请自行科普,
     * 总之该方法返回的值一定是大于或者等于var0的2的整数次方,并且是最接近传入的var0的值的2的整数次方。
     * 例如:传入了12,返回的为16;传入的16,返回的16;传入的17,返回的32。
     */
    static final int tableSizeFor(int var0) {
        int var1 = var0 - 1;
        var1 |= var1 >>> 1;
        var1 |= var1 >>> 2;
        var1 |= var1 >>> 4;
        var1 |= var1 >>> 8;
        var1 |= var1 >>> 16;
        return var1 < 0 ? 1 : (var1 >= 1073741824 ? 1073741824 : var1 + 1);
    }
    

    3.指定“容量大小”的构造函数,不多说,都懂,只是内部再调用上面的构造方法

    public HashMap(int var1) {
        this(var1, 0.75F);
    }
    

    4.传入一个Map类型参数的构造函数:

    public HashMap(Map<? extends K, ? extends V> var1) {
        this.loadFactor = 0.75F; ,// 赋值负载因子的值为默认值0.75
        this.putMapEntries(var1, false); // 将传入的集合中的数据逐个添加到HashMap中
    }
    

    3.常用方法:

    1.put(K var1, V var2):向集合中添加key为var1,value为var2的键值对数据。

    /**
     * 方法内部调用putVal方法,其中第一个参数又调用了hash()方法
     */
    public V put(K var1, V var2) {
        return this.putVal(hash(var1), var1, var2, false, true);
    }
    
    /**
     * 此方法返回根据var0(put方法中传入的key值)得到一个哈希值,首先判断var0是否为空,
     * 如果为空,则直接返回0,如果不为空,返回var0的hashCode()方法返回的值的高16位异或低16位后所得到的值。
     */
    static final int hash(Object var0) {
        int var1;
        return var0 == null ? 0 : (var1 = var0.hashCode()) ^ var1 >>> 16;
    }
    
    /**
     * 此方法才是真正的添加操作,方法内部可以分为几个步骤
     */
    final V putVal(int var1, K var2, V var3, boolean var4, boolean var5) {
        /**
         * 1.创建临时变量var6并指向当前集合中的table,创建临时变量var8并让其等于var6的长度;
         *   如果table为空或者table的长度为0,那么就调用resize()方法进行初始化相关操作。
         */
        HashMap.Node[] var6;
        int var8;
        if ((var6 = this.table) == null || (var8 = var6.length) == 0) {
            var8 = (var6 = this.resize()).length;
        }
        /**
         * 2.创建临时变量var7,并让其指向var6[(var8 - 1) & var1],同时var9为计算出的数组索引值;
         *   如果var7为空,说明在此索引上没有发生哈希碰撞直接调用newNode方法创建一个Node元素并指向table的第var9个索引上。
         */
        Object var7;
        int var9;
        if ((var7 = var6[var9 = var8 - 1 & var1]) == null) {
            var6[var9] = this.newNode(var1, var2, var3, (HashMap.Node)null);
        } else {
            /**
             * 3.如果var7不为空,说明此时发生了碰撞。首先,创建两个临时变量var10和var11;
             *   如果var7的hash值等于var1并且var7的key与var2相等(说明已经存在这个key),此时直接将var10指向var7(其实就是覆盖之前的值)
             */
            Object var10;
            Object var11;
            if (((HashMap.Node)var7).hash == var1 && ((var11 = ((HashMap.Node)var7).key) == var2 || var2 != null && var2.equals(var11))) {
                var10 = var7;
            } else if (var7 instanceof HashMap.TreeNode) {
                /**
                 * 如果不存在这个key,此时判断var7是不是TreeNode(红黑树)类型;
                 * 如果是红黑树类型,那么调用putTreeVal方法给树(var7)插入树节点
                 */
                var10 = ((HashMap.TreeNode)var7).putTreeVal(this, var6, var1, var2, var3);
            } else {
                /**
                 * 进入到这里,说明此时var7还是链表,此时创建int类型临时变量var12并赋值为0(用于记录遍历的次数);
                 */
                int var12 = 0;
                while(true) {
                    /**
                     * 进入循环,遍历链表中的元素,当发现某个元素的next为null时,
                     * 说明该元素为链表中最后一个元素,此时将链表的next指向新的Node节点
                     */
                    if ((var10 = ((HashMap.Node)var7).next) == null) {
                        ((HashMap.Node)var7).next = this.newNode(var1, var2, var3, (HashMap.Node)null);
                        // 如果链表的长度大于或者等于8(var12是从0开始的),将链表转化成红黑树(具体实现在treeifyBin方法中)
                        if (var12 >= 7) {
                            this.treeifyBin(var6, var1);
                        }
                        break;
                    }
                    // 遍历过程中若发现当前元素的hash值和var1相同并且key也和var2相同,会跳出当前循环,并将value更新
                    if (((HashMap.Node)var10).hash == var1 && ((var11 = ((HashMap.Node)var10).key) == var2 || var2 != null && var2.equals(var11))) {
                        break;
                    }
                    var7 = var10;
                    ++var12;
                }
            }
            // 对已经存在的key对应的value进行更新
            if (var10 != null) {
                Object var13 = ((HashMap.Node)var10).value;
                if (!var4 || var13 == null) {
                    ((HashMap.Node)var10).value = var3;
                }
                this.afterNodeAccess((HashMap.Node)var10);
                return var13;
            }
        }
        ++this.modCount;
        // 如果当前集合的大小大于临界值threshold,那么就调用resize()方法重新扩容
        if (++this.size > this.threshold) {
            this.resize();
        }
        this.afterNodeInsertion(var5);
        return null;
    }
    

    以上,就是HashMap在put数据的时候的实现原理,用一张图概括如下:

    hashput.png

    2.get(Object var1):根据key值从集合中获取value

    /**
     * 方法内部创建一个Node类型临时变量,然后调用getNode方法获取对应的Node对象;
     * 如果Node对象为空直接返回null,反之返回Node的value
     */
    public V get(Object var1) {
        HashMap.Node var2;
        return (var2 = this.getNode(hash(var1), var1)) == null ? null : var2.value;
    }
    
    /**
     * 根据var1(get方法中传入的key的hash值)和var2(get方法中传入的key)来返回一个Node对象
     */
    final HashMap.Node<K, V> getNode(int var1, Object var2) {
        // 首先,创建三个临时变量var3(指向table),var4(指向var3[var6 - 1 & var1]),var6(table的length);
        HashMap.Node[] var3;
        HashMap.Node var4;
        int var6;
        // 当table不为空并且table的长度大于0时,判断var3[var6 - 1 & var1]是否为空(这里和put的时候插入的索引运算规则保持一致)
        if ((var3 = this.table) != null && (var6 = var3.length) > 0 && (var4 = var3[var6 - 1 & var1]) != null) {
            // 当var4的hash值和key与入参的hash值和key都相等时,直接返回var4
            Object var7;
            if (var4.hash == var1 && ((var7 = var4.key) == var2 || var2 != null && var2.equals(var7))) {
                return var4;
            }
            // 遍历链表
            HashMap.Node var5;
            if ((var5 = var4.next) != null) {
                // 如果为红黑树类型,就通过getTreeNode方法获取对应的TreeNode
                if (var4 instanceof HashMap.TreeNode) {
                    return ((HashMap.TreeNode)var4).getTreeNode(var1, var2);
                }
                // 不是红黑树,就为链表,遍历链表,当元素的hash值和key值与传入的相等时返回对应的Node
                do {
                    if (var5.hash == var1 && ((var7 = var5.key) == var2 || var2 != null && var2.equals(var7))) {
                        return var5;
                    }
                } while((var5 = var5.next) != null);
            }
        }
        return null;
    }
    

    其实,在把put方法读懂之后,get方法理解起来特别容易了,一个存,一个取,取的时候按照存时的规则去取就可以了。
    3.clear():清空集合数据的方法

    /**
     * 方法内其实就做了两件事,一个是将集合的size置为0,另一个就是遍历table,将里面的元素全部置为null
     */
    public void clear() {
        ++this.modCount;
        HashMap.Node[] var1;
        if ((var1 = this.table) != null && this.size > 0) {
            this.size = 0;
            for(int var2 = 0; var2 < var1.length; ++var2) {
                var1[var2] = null;
            }
        }
    }
    

    4.containsKey(Object var1):判断集合中是否存在var1这个key

    /**
     * 方法内部也是调用了getNode方法,只要getNode方法返回不为空,就说明已经存在这个key值了
     */
    public boolean containsKey(Object var1) {
        return this.getNode(hash(var1), var1) != null;
    }
    

    还有几个常用的方法,比如remove(Object var1),containsValue(Object var1)等,其内部的实现其实都是调用上面讲解过的几个方法,只要把上面的put方法能真正的读懂,其他的方法相信你肯定也能明白。

    5.重量级方法resize():
    此函数有两种调用时机:1.初始化操作(put操作如果table为null时) 2.当前数组容量过小时,需扩容操作。

    final HashMap.Node<K, V>[] resize() {
            HashMap.Node[] var1 = this.table; // 创建var1并指向当前数组table
            int var2 = var1 == null ? 0 : var1.length; // 创建var2使其等于table的长度
            int var3 = this.threshold; // 创建var3使其等于扩容前的临界值
            int var5 = 0;
            int var4;
            if (var2 > 0) { // 当前table长度大于0
                if (var2 >= 1073741824) {
                    // 如果table的长度大于最大容量,那么将threshold置为2147483647,返回当前table,并未做扩容操作
                    this.threshold = 2147483647;
                    return var1;
                }
                // 如果table长度的2倍小于最大容量并且table的长度大于初始的容量16时,进行扩容操作,扩大为原来的2倍(移位运算自行科普)。
                if ((var4 = var2 << 1) < 1073741824 && var2 >= 16) {
                    var5 = var3 << 1; // 新的临界值变为之前的2倍
                }
            } else if (var3 > 0) { // 此时老的临界值已被设置,table还为null
                var4 = var3; // 让新的table数组的容量直接等于老的的临界值
            } else { // 此时临界值未被设置,table也为null
                var4 = 16; // 新的table数组的容量设置成默认值16
                var5 = 12; // 新的临界值设置为12(16 * 0.75)
            }
            // 如果临界值为0,重新计算临界值的大小
            if (var5 == 0) {
                float var6 = (float)var4 * this.loadFactor;
                var5 = var4 < 1073741824 && var6 < 1.07374182E9F ? (int)var6 : 2147483647;
            }
            this.threshold = var5; // 将最终得到的临界值大小赋值给threshold
            // 创建新的Node类型数组并让table指向刚创建的数组var14(新的table数组)
            HashMap.Node[] var14 = (HashMap.Node[])(new HashMap.Node[var4]);
            this.table = var14;
            if (var1 != null) {
                // 把之前的table中的元素一个个的移动到新的table中去(for循环中的移动逻辑这里就不阐述了)
                for(int var7 = 0; var7 < var2; ++var7) {
                    HashMap.Node var8;
                    if ((var8 = var1[var7]) != null) {
                        var1[var7] = null;
                        if (var8.next == null) {
                            var14[var8.hash & var4 - 1] = var8;
                        } else if (var8 instanceof HashMap.TreeNode) {
                            ((HashMap.TreeNode)var8).split(this, var14, var7, var2);
                        } else {
                            HashMap.Node var9 = null;
                            HashMap.Node var10 = null;
                            HashMap.Node var11 = null;
                            HashMap.Node var12 = null;
                            HashMap.Node var13;
                            do {
                                var13 = var8.next;
                                if ((var8.hash & var2) == 0) {
                                    if (var10 == null) {
                                        var9 = var8;
                                    } else {
                                        var10.next = var8;
                                    }
                                    var10 = var8;
                                } else {
                                    if (var12 == null) {
                                        var11 = var8;
                                    } else {
                                        var12.next = var8;
                                    }
                                    var12 = var8;
                                }
                                var8 = var13;
                            } while(var13 != null);
    
                            if (var10 != null) {
                                var10.next = null;
                                var14[var7] = var9;
                            }
                            if (var12 != null) {
                                var12.next = null;
                                var14[var7 + var2] = var11;
                            }
                        }
                    }
                }
            }
            return var14;
        }
    

    关于扩容的逻辑确实有些复杂,方法本身也很长,但其实静下心来一步一步的看,也是可以看懂的。

    总结

    以上,通过阅读源码对HashMap有了更深入一层的了解,其实关于HashMap还有很多需要掌握的知识点的,譬如HashMap是否是线程安全的,它的key是否可以为其他类型,它的key是否可以为null以及它的value是否可以为null等等。单纯就上面讲到的方法来说,我们还需要掌握java的异或,移位,逻辑运算符操作,以及链表和红黑树的知识点。所以,个人认为,一个小小的HashMap还是很考验一个人的水平的,因此真的有必要好好掌握以下。

    写在最后

    PS:没有啥太深的技术功底,只能凭自己的理解再参考一些前辈的文章,有写的不正确的地方还望指出。

    相关文章

      网友评论

          本文标题:Java-从源码理解HashMap

          本文链接:https://www.haomeiwen.com/subject/ezegpctx.html