HashMap源码分析

1 存储数据结构

分析源码之前,先了解两个数据结构,数组和链表。

1.1 数组
  • 内存中分配固定的空间
  • 删除或者增加,导致数组下标内存位移,效率低
  • 数组大小固定不利于扩增
  • 随机读取的效率高,因为数组是连续的,知道内存地址,可以直接获取
Object []  arrays;
1.2 链表
  • 链表的内存大小不固定
  • 删除增加数据方便,因为每一个链表节点存储了节点的数据和下一节点的内存地址(单项或者双向)
  • 查找数据时效率低,因为不具有随机访问性,所以访问某个位置的数据都要从第一个数据开始访问,然后根据第一个数据保存的下一个数据的地址找到第二个数据,以此类推。
  • 方便扩容,增删效率高
    Node {
        Object data;
        Node next;
        Node prex;
    }
1.3 集合特性

对于集合框架我们的关注点一般在一下几点:

  1. 集合底层实现的数据结构是什么 数组+链表+红黑树
  2. 集合中元素是否允许为空 key和value都可以为空
  3. 是否允许重复的数据 key不可以,value可以
  4. 是否有序(这里的有序是指读取数据和存放数据的顺序是否一致) 否
  5. 是否线程安全。 否

2 HashMap

通过上面我们看到数组查询效率高,增删效率低,而链表反之,那么问题来了,有没有一种结构结合二者的优点?我们看下hashMap,直接上码!

依赖关系

HashMap主要是继承自AbstractMap,实现了Cloneable和Serializable接口使得HashMap具有克隆和序列化的功能、实现了Map接口因此具有Map的性质。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 

简单写个main,put etc..

        HashMap map=new HashMap();
        map.put("c","123");

点击put方法进入源码世界

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

看english注解,发现如果你put的key已经存在的话,处理完会覆盖之前的值,并且返回旧值,but putVal后面多了两个参数,先不管他,这个hash(key)有必要了解下,go on

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

h>>>16代表右位移,低位溢出,符号位不变,并用符号位补溢出的高位,那么看下hashCode()方法,打开Object类,可以看到

    public native int hashCode();//c++写的貌似我看不到
    
    public int hashCode() {//这个是String类下重写的
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
    
    public static int hashCode(int value) {//这个是Interger类重写的
        return value;
    }
    //etc...

原来不同对象都可以重写此方法,获取一串不唯一的数字,然后做右移操作,然而这个值来干啥呢?go on!

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                       
                   }

好,来到这里终于知道之前的两个参数啥意思了,putVal(hash(key), key, value, false, true); onlyIfAbsent if true, don't change existing value,evict if false, the table is in creation mode

        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

初始化为空的走扩容,下面着重看下resize方法,只考虑初始化,删除其他的代码

    final Node<K,V>[] resize() {  //通过这个方法也能看出,map的本质是Node数组
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//这个说明鬼,接着往下看
        int newCap, newThr = 0;
        newCap = DEFAULT_INITIAL_CAPACITY;//点击看默认16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//这个是总大小*负载因子,默认0.75
        }
        threshold = newThr;
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//开辟数组
        table = newTab;
        return newTab;
    }

内存有了,接着走,根据hash取存储下标,如果这个地方没有值,那就存数据

        if ((p = tab[i = (n - 1) & hash]) == null)//经过高位运算的hash值和数组的大小-1取与,也就是说取值的范围0-15也就是数组的下标
            tab[i] = newNode(hash, key, value, null);
            
            ································································································································
            //在回顾下node的结构,单链表
            Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;//hash在这里
            this.key = key;
            this.value = value;
            this.next = next;
            }
        

那么问题来了hash值一样了怎么办? 怎么会一样,比如String a,Interger 97

        HashMap map=new HashMap();
        map.put(new String("a"),"a");
        map.put(new Integer(97),97);
        //String hashCode 方法
        public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
        }
        //Interger hashCode 方法
        public static int hashCode(int value) {
        return value;
        }
        

如果存在相同的hash值,那么第二个来的时候必然已经存在

            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p; //如果key是同一个对象,把存在的节点赋值给e,那值怎么办?别着急赋值在后面
            else if (p instanceof TreeNode)//1.8新增的一种结构,稍后看
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                 //这里就是解决hash冲突的地方了
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 多了个限制,防止链表无限扩充吧,稍后分析
                            treeifyBin(tab, hash);
                        break;
                    }
                    //链表已经存在冲突的hash数据,如果key对象和再次传过来的一样,直接返回当前node的节点,即更改引用。
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // 最后复制操作来了
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)//开关+以前的数据为空,就覆盖
                    e.value = value; 
                afterNodeAccess(e);
                return oldValue;
            }

上面代码可以看到遍历当前的Node节点,看他的下一个是不是存在,如果不存在,就放在他下一个的位置
那么极端情况,hash一直冲突,Node就可以无限增长了吗,肯定是不可以的。
TREEIFY_THRESHOLD 看下源码大小限制为8,超过了走treeifyBin(),然后再看下treeifyBin方法

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();//如果小于64继续扩容
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            
            TreeNode<K,V> hd = null, tl = null;
            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)
                hd.treeify(tab);
        }
    }

超过64转树,小于64通过扩容解决,先瞧瞧不到64继续扩容会怎样,继续看下resize方法的实现

        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
        }

进行扩容oldCap << 1 位运算增加一倍,扩容怎么能解决hash冲突?继续往下go
最下面,循环遍历旧的数组,针对没有子节点的链表,重新计算index的位置,赋值。

                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

接着针对有子节点的Node

    /**
     JDK1.7中,resize时,index取得时,全部采用重新hash的方式进行了。JDK1.8对这个进行了改善。
     以前要确定index的时候用的是(e.hash&oldCap-1),是取模取余,而这里用到的是(e.hash &oldCap)
     它有两种结果,一个是0,一个是oldCap,
     比如oldCap=8,hash是3,11,19,27时,
     (e.hash & oldCap)的结果是0,8,0,8,这样3,19组成新的链表,index为3
     而11,27组成新的链表,新分配的index为3+8;
     JDK1.7中重写hash是(e.hash & newCap-1),也就是3,11,19,27对16取余,也是3,11,3,11,
     和上面的结果一样,但是index为3的链表是19,3,index为3+8的链表是27,11,
     也就是说1.7中经过resize后数据的顺序变成了倒叙,而1.8没有改变顺序。
    **/
    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;
    }

也就是说针对e.hash & oldCap=0的保持原来的索引位置不变,为1的原索引位置+原数组的大小,这也是是1.8针对1.7做的优化。回过头来再分析下如果数组Node总大小大于64了会怎样?

        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            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)
                hd.treeify(tab);
        }

接下来分析不下去了,因为要补下红黑树的知识点。。。先分析下get接口,关键代码如下

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

推荐阅读更多精彩内容

  • HashMap 是 Java 面试必考的知识点,面试官从这个小知识点就可以了解我们对 Java 基础的掌握程度。网...
    野狗子嗷嗷嗷阅读 6,604评论 9 107
  • HashMap HashMap概述 HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,...
    史路比阅读 286评论 0 6
  • 一、HashMap概述 HashMap基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用nul...
    小陈阿飞阅读 623评论 0 2
  • Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言。Java 技术具有卓越的通用性、高效性、平台移植性和...
    Java小辰阅读 214评论 0 0
  • 人长了这么多年,一点一点走到今天这个地步,满心以为自己懂得了很多。 听母亲讲话的时候习惯性地说,这些道理我都懂啦,...
    凉已阅读 385评论 -2 1