HashMap源码分析(JDK1.8)

本文将针对jdk8中HashMap用到的几个知识做一个总结,主要涉及到源码及面试中相关的问题

jdk版本 实现
1.8之前 数组+链表
1.8 数组+链表+红黑树

成员变量及常量


    /**
     *初始化容量(必须是二的n次幂)
     */
    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;

    /**
     *放弃链表而使用红黑树的阈值,即链表转用红黑树的阈值(转红黑树条件之一 1/2)
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     *放弃红黑树而使用链表,即链表的值小于6会由红黑树转回链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     *当整个hashMap中元素数量大于64时,且链表的值超过8会转用红黑树(转红黑树条件之一 2/2)
     */
     static final int MIN_TREEIFY_CAPACITY = 64;
     
    /**
     *储存元素的数组(必须是二的幂)
     */
    transient Node<K,V>[] table;

    /**
     *存放缓存
     */
    transient Set<Map.Entry<K,V>> entrySet;
    
    /**
     *map中元素的数量
     */
    transient int size;

    /**
     *该map修改的次数
     */
    transient int modCount;

    /**
     *扩容的临界值
     */
    int threshold;

    /**
     *hash表的负载因子
     */
    final float loadFactor;
    

初始化

先来看看它的构造方法

  1. HashMap(int initialCapacity, float loadFactor);
    自定义初始容量及负载因子
    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(int initialCapacity);
    自定义初始容量,使用默认负载因子 0.75f
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
  1. HashMap();
    最常用的构造方法,默认负载因为0.75f,默认初始化容量 1<<<4
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  1. HashMap(Map<? extends K, ? extends V> m); 接收一个Map类型的集合
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

原来想着重看看使用最多的第三个构造方法,貌似它给负载因子赋了个值然后啥都没干,算了,跳过

重要内部类

链表

这个只有next,并没有prev,它是个单向链表

    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;
        }
    }
红黑树

红黑树的实现,内容有点多,有兴趣的同学可以去研究下,我们只要知道它查询效率高于链表,以及红黑树与链表转换条件就行

   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;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

        /**
         * Returns root of tree containing this node.
         */
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
   }

画图理解下:
HashMap ↓

HashMap内存结构

我大致画了个图,里面有个hash桶(非要说它是个椭圆也行),到目前为止,可以大致了解HashMap的结构了,即:

==数组(transient Node<K,V>[] table)+链表(Node<K,V> )+红黑树(TreeNode<K,V>)==

当然,在一个HashMap实例中,同一个桶(也可以说同一个数组下标中)不会既存在链表又存在红黑树结构,他们会在==链表长度变化时互相转换==。

使用

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

这里它用到了hash(key)这个方法,我们先来看看hash做了什么事

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

先给忘了java位运算的同学回忆下:

  1. ==^==:亦或运算,即==相同取0,不同则取1==

        3^4:
        0011
        0100
        ---------
        0111
        
        结果:7
    
  2. ==>>>==:无符号右移,即忽略符号右移

        注意:无符号右移:高位补0;     有符号右移:正数高位补0,负数高位补1
        
        3>>>2:
        0011
        0000
        ---------
        0000
        
        结果:0
        
        -3>>>2:
        11111111 11111111 11111111 11111101
        00111111 11111111 11111111 11111111
        -----------------------------------
        1073741823
    

int是32位的,上面正数只写了4位是为了方便,不要纠结

可以看出,hash的结果就是==key的hashCode与key无符号右移16位的结果进行亦或运算==,为什么这么做呢?这样可以提高hash的离散性,减少hash碰撞,从而提高效率

扩容

   final Node<K,V>[] resize() {
        //旧表(旧数组)
        Node<K,V>[] oldTab = table;
        //旧数组长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //旧的扩容临界值 默认是负载因子 0.75*初始长度16
        int oldThr = threshold;
        //定义新的数组长度,新的扩容阈值
        int newCap, newThr = 0;
        //如果旧数组初始化过
        if (oldCap > 0) {
            //如果旧数组长度已经不小于集合最大容量,则不能扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果新的数组扩容一倍后小于设定的最大容量,且旧数组长度不小于16,则扩容一倍,且新扩容阈值也增大一倍
            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;
        //旧数组、旧扩容阈值均为0,则取默认值(16,16*0.75)给新的数组
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果新的扩容阈值为0,则用数组长度*负载因子(默认0.75f)
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //给扩容阈值重新赋值
        threshold = newThr;
        //定义新的数组
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //给成员变量table赋值
        table = newTab;
        if (oldTab != null) {
            //遍历旧数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //如果下标为j的桶节点不为null,赋值给e
                if ((e = oldTab[j]) != null) {
                    //设置旧的节点指向null
                    oldTab[j] = null;
                    //如果只存在一个节点直接计算该节点中的元素在新的table中的位置并赋值
                    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 { 
                        //扩容后,一个链表均匀分布到高区与低区
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            /**
                            *e.hash & oldCap 保证了50%的几率将所有元素放置到高、低区
                            *e.hash     xxxx xxxx xxxx xxxx
                            *oldCap     0000 0000 0100 0000 (由于它是2个幂,所以一定是0100这样的格式,即只存在一个1)
                            *结果       取决于e.hash本身与oldCap对应位置的数据为0还是1
                            /
                            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;
    }

注意:链表数据扩容时,用(e.hash & oldCap) == 0来将旧的数组数据==平均地==分布在新的数组中,分==高区==与==低区==

添加(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;
        //如果容器尚未初始化,则初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //计算该元素的位置,如果该位置没数据,则直接放入一个新的链表对象并存入该元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        //hash碰撞后的处理
            Node<K,V> e; K k;
            //如果之前存在与该元素key相同的元素,则替换之前的元素!!!暂时跳过,后面会替换(注意:这里要先判断hash码是否相等,所以两个元素equals时,放入同一个map不一定会被替换)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //如果桶的数据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) {
                    //如果下一个节点为null,则直接放入
                        p.next = newNode(hash, key, value, null);
                        //如果此时节点长度达到了转红黑树的阈值(为什么是8-1=7?因为放入了当前这个元素,长度就达到了8),则进行红黑树的转换判断,是扩容?(MIN_TREEIFY_CAPACITY=64 记得这个参数吗?map长度如果小于64则扩容)还是转红黑树?(TREEIFY_THRESHOLD达到了8与map长度达到MIN_TREEIFY_CAPACITY 64)
                        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))))
                        break;
                    p = e;
                }
            }
            //遍历完后e还有值,说明遇到了需要替换的元素
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //如果onlyIfAbsent为false或者旧值为null,则替换旧值,所以参数onlyIfAbsent的意思就是,当旧值存在时,遇到一样的元素,是否进行替换,默认为false,就是要替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                    //将新put的元素移动到最后一个节点,我感觉这里应该是用来更新元素的年龄,有点像jvm中老年代的感觉
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //修改次数+1
        ++modCount;
        //扩容判断
        if (++size > threshold)
            resize();
            //根据传入参数判断是否移除该链表(红黑树)中最老的节点
        afterNodeInsertion(evict);
        return null;
    }

大概就是这么回事儿,我觉得主要应该关注有一下几点

1.当发生hash碰撞时它做了什么事?
根据入参onlyIfAbsent(默认false)判断(如果旧值为null则直接替换)是否替换,如果为false则替换之前的元素
2.为什么要将新加入的元素放置在链表尾端?(红黑树也继承于Node)
我猜想应该是将新旧元素进行排序,最旧的在head,而最新的在tail

获取(Get)

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;
        //判断map是否为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //检查第一个节点是否命中
            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;
    }

移除(remove)

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //判断该key对应节点是否存在
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //直接定位到下标 (n - 1) & hash 的桶,并判断头部节点是否命中
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
            //红黑树遍历查找
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                //链表遍历查找
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //根据传入参数判定值 value
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                                 //移除红黑树中的节点
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                    //移除链表中的节点
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

hash

在hashMap中大量使用了hash

  1. 计算key的hash值时,==(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)==;
  2. 计算数据存放位置时,==e.hash & (newCap - 1)==

相关问题

1.为什么HashMap容量需要2的幂?

在计算桶的位置时,会通过hash&(size-1)来确定该map在数组中的下标,这里设计size(HashMap)的容量,就是为了在运算时,保证size-1在做位运算时,永远以...1111结尾,即:


        hash:      xxxx xxxx 0101
        size-1:    0000 0000 1111
        结果:      0000 0000 0101 = 5
        
HashMap就是通过这种方式来保证元素的均匀分布

2.HashMap线程不安全主要是在哪个地方?

HashMap的不安全主要体现在两个地方:
ps:jdk1.7之前在多线程环境下,扩容的时候,链表可能会行成一个闭环,当我们在查询一个不存在的key调用get()时,遍历链表进入死循环,但是1.8已经不存在这个问题了,原因就是jdk1.7对链表元素进行了一次倒序处理,而jdk1.8中使用了正序处理。
   1、数据丢失
   2、size()不准确

原因的话,可以参考大佬写的:https://blog.csdn.net/fumitzuki/article/details/82760236

3.为什么重写equals时一定要重写hashCode?

其实从我们了解了HashMap源码后,就能从这个角度来解释这个问题,打个比方,如果一个对象O重写equals但是并未重写hashCode,在存放到HashMap时及操作HashMap时均会出现一系列问题;
put:由于未重写hashCode,在我们存入对象时,两个equals的对象hashCode可能是不等的,那么在通过 e.hash & (newCap - 1) 计算元素位置时,极有可能放置到不同的地方,即map中会存放两个我们认为是一样的对象;就算过了第一关,hash计算出来的位置相等,也会在插入同一个链表时,判断两个元素相等,HashMap中的逻辑是先判断hashCode是否相等,所以还是存放了两个我们认为一样的对象。
get:取的时候,也会根据hashCode计算位置,所以我们取出来的对象很可能不是我们需要的那一个,当然了remove也是一样的道理

作者能力有限,如果大家发现有不对的地方或者疑问,欢迎指正,大家一起进步!3q

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容

  • hash概念 把关键字通过某个函数映射到得到一个固定值,然后这个固定值来确定数组中的某个位置,通过数组下标一次定位...
    阿狸404阅读 394评论 0 4
  • HashMap源码分析——JDK1.8 转载:http://blog.csdn.net/u010498696/ar...
    SinX竟然被占用了阅读 272评论 0 0
  • 前言 本文将对HashMap(基于JDK1.8)的源码进行具体分析,包括构造方法以及增、删、改、查等基本操作。 源...
    MrFengZH阅读 262评论 0 0
  • *背景人设有自我发挥/标题瞎取,暂定/渣文笔 Credence非常迷恋Graves的拥抱——这个秘密他没有告诉任何...
    怪怪怪怪怪阅读 746评论 0 0
  • 一省目标 春节后本来打算跳槽的,最终又觉得留在原公司,兜兜转转,还是回到原点。制定了新的目标,可是最近又觉得很迷茫...
    Carol在路上阅读 410评论 2 0