zoukankan      html  css  js  c++  java
  • 【java集合框架源码剖析系列】java源码剖析之HashMap

    前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几天可能会和你联系来看当时以为自己一面应该是通过的,但是事与愿违,痛定思痛,仔细回顾了一下面试官问我的整个过程,感兴趣的可以参看我的博客:【阿里内推一面】记我人生的处女面。感觉自己回答的不是很好的地方就是关于java方面的,所以下定决心将java中的核心知识来个大的梳理,java中的核心知识可能很多,但在我看来大的类型就那么几个,其它的只是属于细枝末节而已,主要是java集合框架,javaI/O体系,java中的线程,Java类的加载与JVM(包括java内存模型)这四大块。其中最核心的当属java集合框架,java中的线程与JVM相关的知识。

    之所以最先讲解java中的HashMap是因为HashMap应该是我们在做项目中用到的最多的处理复杂数据的集合,可以说使用率一点都不低于ArrayList与LinkList。

    注:本人的源码基于JDK1.8.0,JDK的版本可以在命令行模式下通过java -version命令查看。

    一首先我们来看一下HashMap类的定义:

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {
    
        private static final long serialVersionUID = 362498820763181265L;<span style="font-family: Arial, Helvetica, sans-serif;">}</span>

    从上述JDK源码可以看到:

    1HashMap继承自AbstractMap类同时实现了Cloneable,Serializable这两个接口,其中前一个接口Cloneable是为了实现clonet()机制,Serializable接口是为了实现序列化机制,关于这两种机制的相关知识再此不做赘述。

    2HashMap用到了泛型来实现参数化类型,其实java中的全部集合框架都使用到了泛型。

    二:HashMap中一些重要的成员属性:

    /**
         * The default initial capacity - MUST be a power of two.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    
        
        static final int MAXIMUM_CAPACITY = 1 << 30;//Hash数组的最大容量,该值必须为2的n次,最大为1^32, MUST be a power of two <= 1<<30.
         * The load factor used when none specified in constructor.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认装载因子
       transient Node<K,V>[] table;//用来存储数据的数组,每个元素都是Node即链表
    
        static final int TREEIFY_THRESHOLD = 8;//当add一个元素到某个位桶,其链表长度达到8时将链表转换为红黑树
        
        transient Set<Map.Entry<K,V>> entrySet;
          transient int size;
    // The next size value at which to resize (capacity * load factor).   int threshold;//临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩
    


    其中transient关键字是用来让该域在整个类被序列化的时候不包含该内容,即该域不被序列化。至于每个属性的含义,上述代码英文注释很详细。重点说一下static final float DEFAULT_LOAD_FACTOR = 0.75f;这个属性,该属性即为装载因子,本质上就是我们学习数据结构中解决Hash冲突的填充因子的意思,它的默认值是0.75,如果实际元素所占容量占分配容量的75%时就需要扩容。

    三HashMap的内部实现原理:我们来看一下HashMap的构造器

        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);
        }
    
       
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    
       
        public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    
       
        public HashMap(Map<? extends K, ? extends V> m) {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            putMapEntries(m, false);
        }
    
    可以看到java设计者们重载了4个HashMap的构造器,重点关注一下tableSizeFor(),putMapEntries这两个函数,我们来看一下tableSizeFor的源码:

    /**
         * Returns a power of two size for the given target capacity.
         */
        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;
        }
    

    从注释上就可以清楚的知道该函数的功能就是保证HashMap的容量Capacity属性总是2的n次方,之所以这么做原因在于确保Hash散列的均匀性,为何这样做就能保证Hash的均匀性呢?这就需要看HashMap中Hash()函数的源码:

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
    
    可以看到hash函数就是通过key.hashCode()得到int类型的h,然后通过 h&(h-1)得到所在数组位置,注意>>>运算符表示无符号右移运算,所以结果就很清楚了,h为2的整数幂保证了h-1最后一位(当然是二进制表示)为1,从而保证了取索引操作 h&(length-1)的最后一位同时有为0和为1的可能性,保证了散列的均匀性。反过来讲,当Hash表长度length为奇数时,length-1最后一位为0,这样与h按位与的最后一位肯定为0,即索引位置肯定是偶数,这样数组的奇数位置全部没有放置元素,浪费了大量空间。总之:length为2的幂保证了按位与最后一位的有效性,使哈希表散列更均匀。

    接着我们来看一下:putMapEntries这个函数:

      final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
            int s = m.size();
            if (s > 0) {
                if (table == null) { // pre-size
                    float ft = ((float)s / loadFactor) + 1.0F;
                    int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                             (int)ft : MAXIMUM_CAPACITY);
                    if (t > threshold)
                        threshold = tableSizeFor(t);
                }
                else if (s > threshold)
                    resize();
                for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                    K key = e.getKey();
                    V value = e.getValue();
                    putVal(hash(key), key, value, false, evict);
                }
            }
        }
    可以看到首先获得传入的map实例的大小s,然后存在一个将大小s与临界值比较的过程,如果map实例的大小大于threshold(即零界值的大小),则调用resize()方法,即扩容,我们来看一下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;
            if (oldCap > 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; // 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];//27行
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        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
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }

    从上述代码可以看到

    1首先定义了一个临时数组oldTab来保存table,然后获取该table的大小oldCap,如果oldCap的值大于MAXIMUM_CAPACITY(即HashMap所允许的最最大容量1>>30),则无法扩容,只能更改 threshold(即扩容的临界值)的值,否则newCap = oldCap << 1,即令新的容量为原来的2倍,且oldCap >=DEFAULT_INITIAL_CAPACITY(从上上面HashMap中重要成员属性这块可以看到值为16),则将新的临界值也更改为原来的2倍,即newThr = oldThr << 1;,即扩容机制包括两部分:1HashMap中table数组的容量的扩容,和成员属性threshold(即扩容的临界值)的更改。

    2从上述代码的第27行可以看到,扩容之后需要创建一个新的Table数组(该数组中的每个元素为Node类型,即链表类型)   Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];然后将原数组中的类容重新计算hash值放到新的数组中,分为两种情况讨论,这两种情况对应两种不同的数据结构类型,即链表和红黑树。及对应上述代码中的Node与TreeNode,如下所示:

    else if (e instanceof TreeNode)
             ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//如果为红黑书,则调用split()
    else{...}//否则,表示为链表节点
    看到这就不得不提一下HashMap内部扩容机制所涉及到的数据结构链表Node与红黑树TreeNode了,这两个数据结构为HashMap中的内部类,源码如下:

     static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
    
            Node(int hash, K key, V value, Node<K,V> 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; }
    
            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;
            }
        }
    可以看到该链表Node是一个单向链表(因为只存在一个Node<K,V> next属性)它实现了Map.Entry<K,V>接口。

     static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
            TreeNode<K,V> parent;  // red-black tree links
            TreeNode<K,V> left;
            TreeNode<K,V> right;
            TreeNode<K,V> prev;    // needed to unlink next upon deletion
            boolean red;//表示颜色的属性,红黑树是一种自平衡二叉查找树,用red与black来标识某个节点,它可以在O(logn)内进行查找,插入与删除
            TreeNode(int hash, K key, V val, Node<K,V> next) {
                super(hash, key, val, next);
            }
    可以看到TreeNode它实现了LinkedHashMap.Entry<K,V>接口.

    看到这里我们也就明白了HashMap的底层实现原理了,即HashMap是采用数组 Node<K,V>[]table来存储<K,V>的,数组中的每个元素是Node类型(可能会将该Node类型转换为TreeNode类型),通常称这种方式为位桶+链表/红黑树,当某个位桶的链表的长度达到TREEIFY_THRESHOLD临界值的时候,这个链表就将转换成红黑树。本质上是一个Hash表,用来解决冲突的(这一点将在HashMap中的put<K,V>方法中看到)。用图示表示如下:


    四HahMap常用的方法:

    1put(K,V);

    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;
            if ((tab = table) == null || (n = tab.length) == 0)//1
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)//2首先判断tab[(n - 1) & hash]处是否为空,如果是代表该数组下标为[(n - 1) & hash]的位置无元素,可直接put
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            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))))//如果Hash值相同,则调用equals方法来确定是否存在该元素,则执行break语句
                            break;//跳出for循环,执行下面的if语句,即<span style="font-family: Arial, Helvetica, sans-serif;">existing mapping for key,则更新value的值,e.value=value。</span>
    
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);//
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    可以看到

    1在注释1处,首先判断table数组的长度是否为0或table数组是否为空,即通常情况下表示刚创建一个空的HashMap时,当你调用put(K,V)方法时才会分配内存,即tab = resize()

    if ((tab = table) == null || (n = tab.length) == 0)//1
                n = (tab = resize()).length;
    2在注释2处,首先判断tab[(n - 1) & hash]处是否为空,如果是代表该数组下标为[(n - 1) & hash]的位置无元素,可直接put,否则存在元素,出现冲突,则解决冲突,分为链表与红黑树这两种情况。

    即put方法主要包括两大部分:

    1根据传入的key计算hash值得到插入的数组索引i,如果tab[i]==null,表示此下标处无元素存在,可直接添加元素,否则出现冲突,那么就要用到链表或红黑树来解决冲突,可参看上图帮助理解,

    2如果出现冲突,则扫描链表或红黑树,在此过程中通过equals方法来确定是否存在该元素,如果存在,则直接更新,否则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中,可参看上图帮助理解。

    即通过hash的值来判断是否存在该元素,如果hash值不存在(tab[i]==null),则一定不存在该元素,若hash值存在,则可能存在该元素,需要通过equals方法来确定,如果hash值存在且key.equals.(k)则表明存在该元素,直接更新其值,否则表明不存在,则采用链表或红黑树的方式将元素添加到tab[i]对应的链表或红黑树中

    2 V get(Object key) 

     public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
    
    
    final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {     //通过该hash值与table的长度n-1相与得到数组的索引first
                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)//代表该HashMap为数组+红黑树结构
                        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某个元素与put某个元素是一一对应的关系,即先通过key得到对应的hash值,然后通过该hash值与table的长度n-1相与得到数组下标的索引first,然后先判断传入的hash是否与数组索引first节点对应的hash值相等,如果是则直接返回该数组元素first,否则则通过first.next不断查找该数组元素所对应的链表/红黑树中是否存在hash与key均和传入的hash与key相等的节点,如果存在则代表在HashMap集合中找到了该元素,则返回其对应的Value。

    五总结:

    1HashMap内部是基于Hash表实现的,该Hash表为Node类型数组+链表/红黑树,其中链表与红黑树是用来解决冲突的,即当往HashMap中put某个元素时,相同的hash值的两个值会被放到数组中的同一个位置上形成链表或红黑树。

    2HashMap存在扩容机制,是通过resize()方法实现的,即当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,数组的大小*loadFactor=threshold(即扩容的临界值),默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍

    3另外从put与get的源码可以看到HashMap的Key与Value都允许为null,同时可以看到HashMap中的put与get方法均无synchronized关键字修饰,即HashMap不是线程安全的。

    4HashMap中的元素是唯一的(即同一个key只存在唯一的V与之对应),因为在put的过程中如果可能出现相同元素(K相同V不同),则原来的V将会被替换。
















  • 相关阅读:
    容器跨主机网络通信学习笔记(以Flannel为例)
    Kubernetes控制器Job和CronJob
    记一次使用Flannel插件排错历程
    Kubernetes控制器Deployment
    Kubernetes如何通过StatefulSet支持有状态应用?
    react18 来了,我 get 到...
    gojs 实用高级用法
    vuecli3 vue2 保留 webpack 支持 vite 成功实践
    calibre 报错 This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem. 解决
    unable to recognize "*.yaml": no matches for kind "RoleBinding" in version "rbac.authorization.k8s.io/v1beta1"
  • 原文地址:https://www.cnblogs.com/hainange/p/6334040.html
Copyright © 2011-2022 走看看