HashMap源码之构造方法

一、HashMap数据存储结构

HashMap底层就是一个哈希表,即一个数组加链表结构,在java8中如果链表长度达到8则转化为红黑树结构,具体操作过程后面会讲到,先看看最终存储元素后HashMap的结构,Entry为HashMap内部维护的一个静态内部类,在java8改名为Node
数据存储方式

下面先来看看静态内部类Node,它其实就是一个单向链表的节点类,hash存储key对应的hash值,key和value即存储保存在HashMap中的key-value键值对,next是一个指向下一Node节点的引用

static class Node<K,V> implements Map.Entry<K,V> {
        //key对应的hash值
        final int hash;
        //用户存储的key
        final K key;
        //用户存储的value
        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;
        }
        //getter&toString方法
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        //hashCode方法
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //setter方法
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //equals方法
        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;
        }
    }

二、HashMap源码中几个重要属性

1、首先看几个静态常量
    //默认初始容量,必须是2的幂,如果使用默认构造方法,则Node数组的默认长度则为该值
    //至于长度为什么必须是2的幂,后面讲put方法时会讲到
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //Node数组的最大容量,如果使用有参构造且指定容量大于该值,则使用该值,扩容时当容量
    //达到该值时不在扩容,因为int最大值为1<<31-1,非2的幂,所以最大值只能设置到1<<30
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //默认加载因子,HashMap实际存储元素个数为Node数组容量*加载因子,当数组容量达到上面
    //的最大值时,实际容量理论上为无限大
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //链表转换红黑树的阀值,当hash冲突导致链表长度达到此值时转换为红黑树,此为java8的优化
    static final int TREEIFY_THRESHOLD = 8;

    //红黑树转链表的阀值,在HashMap扩容时,如果红黑树长度小于等于此值时,红黑树拆分为链表
    static final int UNTREEIFY_THRESHOLD = 6;

    //此处需注意,java8之前是当Node节点数超过HashMap实际容量(即数组容量*加载因子)时扩容,
    //而在java8中,当数组容量还没达到下面这个值时,只要链表长度达到树化阀值也就是8即扩容       
    static final int MIN_TREEIFY_CAPACITY = 64;
2、HashMap中的属性值
    //定义Node数组
    transient Node<K,V>[] table;

    //定义Node节点的Set集合(从上面看的到Node实现了Map.Entry<K,V>接口),遍历的时候用的多
    transient Set<Map.Entry<K,V>> entrySet;

    //定义HashMap中Node节点的数量,也即key-value键值对数量
    transient int size;

    //定义HashMap结构变化的次数,put,remove等方法就会改变此值
    transient int modCount;

    //定义HashMap的实际容量(Node数组*加载因子)
    int threshold;

    //定义加载因子
    final float loadFactor;

三、HashMap的几个构造函数

HashMap一共四个构造函数,下面分别讲一下:

1、默认构造方法
    //默认构造方法就是把加载因子赋值为默认值,即0.75
    //但后面会看到,虽然没有显示初始化Node数组容量,但默认容量为16
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
2、单参数构造方法
    //此构造方法会指定初始化Node数组容量,加载因子还是默认值0.75,然后调用自己的双参构造
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
3、双参数构造方法
    //此构造方法接收两个参数,初始数组容量和加载因子
    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;
        //给定义的实际容量赋值,这里有个疑问,实际容量不是等于initialCapacity*loadFactor吗?
        //首先tableSizeFor()方法是求大于等于给定值的最小2的幂,此方法等下讲,超级高明的实现
        //然后因为所有构造方法都没有初始化上面的table,所以第一次put就会调用resize方法,然后
        //重新初始化threshold ,此时threshold就符合数组容量*加载因子的规则了
        //具体put及resize方法请参看我的相关文章
        this.threshold = tableSizeFor(initialCapacity);
    }

HashMap源码之put方法
HashMap源码之resize方法
下面看看tableSizeFor()方法:

    //此方法返回大于等于给定值的最小2的幂,例如:tableSizeFor(10)=16,tableSizeFor(20)=32
    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;
    }

下面以10为例讲一下这个方法:
首先int n = 10 - 1,则n=9,下面看看9的二进制数
1001 -------9
0100 -------无符号右移一位后
1101 -------上面两个值|(或)运算后
0011 -------上面的值再无符号右移两位
1111 -------上面两个值|(或)运算后
后面的几步右移4,8,16后的值都是0000,或运算后都为1111
所以最后n的二进制数为1111,即为15,最后小于默认最大值,所以n+1之后等于16,即2的4次幂
此方法就是把给定值的二进制数最高位右边的所有位数变为1,然后加1,则必为2的幂
例如有一个数:11010001000110001011101110 对应十进制:54813422
经过tableSizeFor()几波右移或运算后得到:11111111111111111111111111,然后+1
得到:100000000000000000000000000 即2的26次幂
开头先-1是为了防止要操作的数本身就是2的幂,然后得出来的就直接是此值的两倍了,如果本身值比较大,那这个值最终作为Node数组的长度就太浪费空间了

4、直接传入Map集合的构造方法
    //此构造方法传入一个Map集合,设置加载因子为默认值,然后调用putMapEntries()
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

下面来看看putMapEntries()方法:

    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        //定义int值s等于传进来的Map的Node节点数量
        int s = m.size();
        //判断Map集合是否是空,是空不做任何处理,那样的话,上面的构造方法等同于默认构造
        if (s > 0) {
            //如果初始的table为null,则走这里
            if (table == null) { // pre-size
                //用s除以加载因子,得出预测装入s数量元素需要的Node数组大小,+1.0补精度
                float ft = ((float)s / loadFactor) + 1.0F;
                //如果上步得出的ft大于默认最大数组长度,则直接赋予最大值,不然等于ft的int值
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果最终得出的值t大于本来的threshold,则要对t使用tableSizeFor()方法
                //得出得值再赋予threshold
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //如果初始的table不为null,并且传进来的集合元素个数大于此集合的实际容量,则直接扩容
            else if (s > threshold)
                resize();
            //最后循环传入的Map集合,依次调用putVal方法将元素插入现在的Map中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                //putVal()方法将在讲put方法的时候讲到
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

注意:
HashMap构造方法传入的初始化数组长度值initialCapacity是为Node数组的初始化长度,虽然在构造方法各处都使用了this.threshold = tableSizeFor(initialCapacity),并且在tableSizeFor()方法中并没有使用initialCapacity*loadFactor,但在第一次添加元素时会初始化,具体的在讲put方法时会说明

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

推荐阅读更多精彩内容