JAVA源码剖析(容器篇)HashMap解析(JDK7)

Map集合:

JAVA源码剖析(容器篇)HashMap解析(JDK7)

HashMap底层结构示意图:

HashMap是一个“链表散列”,其底层是一个数组,数组里面的每一项都是一条单链表。

数组和链表中每一项存的都是一“Entry对象”,该对象内部拥有key(键值名),value(键值),next(指向下一个结点)等属性。

JAVA源码剖析(容器篇)HashMap解析(JDK7)

一、HashMap属性:

内部属性源码解析:

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容器初始容量,出于hash计算时的优化的考虑,必为2^n,此处为16

    static final int MAXIMUM_CAPACITY = 1 << 30;// 默认最大容量

    static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认的负载因子,关系到容器何时扩容

    static final Entry<?,?>[] EMPTY_TABLE = {};//扩容之前的默认数组

    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;// 用来存储数据(单向链表)的数组,长度就是容器长度即2^n

    transient int size;// 当前map的key-value映射数

    int threshold;// 下一次扩容的大小(第一次创建空hashmap时,会上传一个该值,用于下一次创建table时设置大小)

    final float loadFactor;// 自定义的负载因子,关系到容器何时扩容

    transient int modCount;// Hash表结构性修改次数,用于实现迭代器快速失败行为

    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;//容量阈值的默认大小

    transient int hashSeed = 0;//该值在计算hash值时会用到,大小与table容量有关,由于初始table为空,所以此值初始也为空

二、HashMap构造:

构造函数poublic源码解析:

    //生成空hashmap时指定容量大小和负载因子
    public HashMap(int initialCapacity, float loadFactor) {

        if (initialCapacity < 0)//初始容量大小不能小于0
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);

        if (initialCapacity > MAXIMUM_CAPACITY)//保证初始容量不大于默认的最大容量
            initialCapacity = MAXIMUM_CAPACITY;

        if (loadFactor <= 0 || Float.isNaN(loadFactor))//负载因子不能小于0,且只能为数字
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);

        this.loadFactor = loadFactor;//设置负载因子

        threshold = initialCapacity;//设置下次创建数组的大小。此时因为建的是个空map,并不会立刻通过该值初始化数组,所以这个初始化大小值会先存起来,当以后put数据后,再根据该值创建初始数组的大小

        init();//进行一些必要的初始化,但在hashmap中没有实现,init()只在LinkedHashMap中有实现
    }

    //生成空hashmap时只指定初始容量大小,负载因子使用默认值
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //生成空hashmap时,初始容量和负载因子都是用默认值
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    //构造hashmap时传入一个map,负载因子使用默认值,初始容量与传入的map的大小有关
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

        inflateTable(threshold);//对table扩容(新建一个table)

        putAllForCreate(m);//将指定map的键值对添加到当前map中
    }

相关private、friendly源码解析:

    //将指定map的键值对添加到当前map中
    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())//利用foreach迭代出传入map里的每一项entry对象
            putForCreate(e.getKey(), e.getValue());//调用私有方法存入该entry。(该私有put与public的put的主要区别是,该私有put不会扩容table(因为之前已经扩过了),也不会增加modCount)
    }

    //存入键值对
    private void putForCreate(K key, V value) {

        int hash = null == key ? 0 : hash(key);//根据key值生成hash值

        int i = indexFor(hash, table.length);//根据hash值生成数组下标索引

        //根据新存入值的下标,查询现有数组中该下标里是否有元素,如果有元素,由于该元素一定是以链表的形式存储的,则将插入元素的key与查询链表里的每一项元素的key进行比对
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }

        createEntry(hash, key, value, i);//在指定索引处的链表上创建该键值对
    }

    //在table数组的某一项中,创建单向链表的表头
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

Entry类源码解析:

    //在数组的每一项里存储了一条单向链表,而每一条链表里存的都是这种Entry对象。key-value实际上就是被存成了这种Entry对象
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;//键值名
        V value;//键值
        Entry<K,V> next;//指向单向链表中的下一个entry对象
        int hash;//hash值

        //创建Entry对象时的构造函数
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        //获取entry的key
        public final K getKey() {
            return key;
        }

        //获取entry的value
        public final V getValue() {
            return value;
        }

        //用新value覆盖旧value,并返回旧value
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        //如果传入的是个Entry对象,则判断其key和value是否相同,相等则返回true
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        //key与value的hashcode()做^运算
        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        //每当相同key的value被覆盖时被调用一次,HashMap的子类LinkedHashMap中实现了这个方法
        void recordAccess(HashMap<K,V> m) {
        }

        //每移除一个entry就调用一次
        void recordRemoval(HashMap<K,V> m) {
        }
    }

相关说明:

1、从源码可看出构造函数主要是负责在构造时指定“初始容量”和“负载因子”。

2、如果此时创建的是一个空map,则并不会直接创建其内部table数组,而是等到有数据添加进来后再创建。

但如果此时是根据传入的新map来构建此hashmap,则会先创建一个空hashmap,再根据初始化容量创建一个table数组,之后再将传入的map里的数组存入刚刚创建的table中。

3、关于负载因子(loadFactor),表示当前容量达到总容量的什么程度时再扩容。

如,loadFactor为0.75,且设置hashmap最大容量是16。当当前的数据量达到12时(16*0.75=12),就会进行扩容。同理,如果loadFactor为1,则当前的数据容量达到16时才会扩容。

4、“初始容量”在设置时不需要设置为2^n的形式,但在使用该值扩容(创建数组)时,就会查找“大于该值的最小2^n”。

三、HashMap存储:

相关public源码解析:

    //向hashmap存入一个键值对,如果该key已经存在,则覆盖该key的内容
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {//如果此时数组为空,数组为空,则新建数组(扩容数组)
            inflateTable(threshold);
        }

        if (key == null)//hashmap中允许key为null,如果key为null,则对value的存储位置特殊处理
            return putForNullKey(value);

        int hash = hash(key);//根据key值,生成hash值

        int i = indexFor(hash, table.length);//根据hash值找到该值在table数组中的下标索引

        //根据新存入值的下标,查询现有数组中该下标里是否有元素,如果有元素,由于该元素一定是以链表的形式存储的,则将插入元素的key与查询链表里的每一项元素的key进行比对
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//如果新插入元素的key与链表中元素的key的“equals()与hashCode()”都相等,则覆盖value
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;//该hashmap的操作次数+1,与迭代器迭代有关

        addEntry(hash, key, value, i);//运行此方法表示该key值是新key,所以将存入的键值对变成一个“Entry对象”存入数组。

        return null;
    }

    //将指定map中的元素存入此hashmap
    public void putAll(Map<? extends K, ? extends V> m) {
        int numKeysToBeAdded = m.size();//传入map的key-value映射数量

        if (numKeysToBeAdded == 0)//传入map中的映射数量为0则不存
            return;

        if (table == EMPTY_TABLE) {//判断当前hashmap中是否有初始table数组,没有则创建(扩容)初始table
            inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
        }

        if (numKeysToBeAdded > threshold) {//传入map的key-value映射数量如果大于当前的“扩容大小”,则判断是否扩容
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)//与hashmap容量默认最大值比较,不能大于最大值
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)//如果当前数组长度小于targetCapacity,则扩大一个“2次方”
                newCapacity <<= 1;//按位运算,相当于扩大了一个平方
            if (newCapacity > table.length)//如果newCapacity扩大完后大于了当前数组的长度,则重新扩容数组
                resize(newCapacity);
        }

        //foreach传入的map,将里面的每个entry对象存入此map
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }

相关private、friendly源码解析:

相关说明:

四、HashMap提取:

五、HashMap遍历:

六、HashMap线程相关:

七、HashMap其他注意事项:

1、只有JDK7之前是这一套底层结构,目前最新的JDK8中HashMap当同一个hash值的节点数大于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。

上一篇:Learning Puppet — Resource Ordering


下一篇:[oracle] update和merge语句的几点写法