Java底层之HashMap底层实现原理

HashMap简介

      HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。 HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。 HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,大小为16。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度,大小为0.75。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

源码解析

      HashMap底层使用EMPTY_TABLE数组来存储key/value

//HashMap使用Entry类型的数组存储
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

      Entry类结构,当存在hash冲突时,entry的next变量指向冲突链表的下一entry

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next; //指向链表的下一个entry节点
        int hash;  //此entry的hash值

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }
        //修改当前entry的value,返回旧的value
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //判断两个entry是否相等
        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();
            //比较key
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                //比较value
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
        //计算hash值--key和value的hash值的二进制异或作为entry的hash值
        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

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

        void recordAccess(HashMap<K,V> m) {
        }

        
        void recordRemoval(HashMap<K,V> m) {
        }
    }

      1.  当创建一个HashMap对象时,初始化一个存放Entry的数组,大小为16;

//数组容量的初始大小为16,1左移4位
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  
//数组的最大容量为1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子为0.75,当数组容量*加载因子小于Entry的个数(size)时,扩大数组的容量为当前数组大小的两倍,全部entry重新进行Hash值计算进行散列
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当前HashMap存储的entry个数
transient int size;

      2.  put方法及其调用方法

       (1)put方法

    //put方法
    public V put(K key, V value) {
        //此处相当于懒加载,当数组未创建时,进行数组的初始化,大小为16
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //当key为null时,放在数组的第一位,即Table[0]
        if (key == null)
            return putForNullKey(value);
        //根据key计算hash值
        int hash = hash(key);
        //根据hash值和数组的长度计算entry需要放置的数组下标,方法见(2)
        int i = indexFor(hash, table.length);
        //循环遍历当前位置的链表,如果存在key相同的,则用新的value替换掉旧的value
        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))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //修改次数增加,用于线程安全判定
        modCount++;
        //增加元素,方法见(3)
        addEntry(hash, key, value, i);
        return null;
    }

       (2)indexFor 方法,计算存放的下标

    /**
     * @param h  Hash值
     * @param length  HashMap底层数组长度
     * index的计算方式为二进制&运算,因为数组的下标为0-15,所以用length-1进行运算;
     * 其次length的数值为16为基础,逐次2倍扩大。以16为例,二进制为10000,当10000与其它数字进行与运算时,产生的值只有0或者16,会产生大量的Hash冲突;当采用15时,1111和其它数值&计算结果会分布较为均匀。
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

        (3)addEntry:增加元素,判断是否需要扩容

    /**
     * @param hash 经过key计算出来的hash
     * @param key  插入的元素的key
     * @param value  插入元素的value
     * @param bucketIndex  插入数组的下标
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // threshold = capacity * loadFactor,即数组长度 * 加载因子(0.75),作为阈值
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //达到阈值,数组扩容为原来的2倍,方法见(4)
            resize(2 * table.length);
            //因为数组改变,重新进行计算hash值和数组索引
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //没有达到阈值,增加元素, 方法见(5)
        createEntry(hash, key, value, bucketIndex);
    }

        (4)resize 方法:扩容,重新生成数组进行所有元素的散列

   /**
     * @param newCapacity 新数组的大小
     */
    void resize(int newCapacity) {
        //取到原数组
        Entry[] oldTable = table;
        //原数组长度
        int oldCapacity = oldTable.length;
        //最大长度
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //创建一个新数组,长度为原数组的2倍
        Entry[] newTable = new Entry[newCapacity];
        //所有元素进行hash值和数组索引的计算,重新散列
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        //重新计算阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

        (5)createEntry 方法:添加元素,并解决hash冲突

    /**
     * @param hash 经过key计算出来的hash
     * @param key  插入的元素的key
     * @param value  插入元素的value
     * @param bucketIndex  插入数组的下标
     * 插入元素,并解决hash冲突
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        //如果当前节点存在其它元素,则发生hash冲突,由新插入的元素顶掉原位置的元素,新元素的next指向被顶掉的旧元素,即新元素是从链表头部插入,e为链表的头元素
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //元素个数增加
        size++;
    }

      3.  get方法及其调用方法

        (1)get方法

    /**
     * @param key 要获取元素的key
     */
    public V get(Object key) {
        //如果key为null,从存储key=null的位置获取元素,方法见(2)
        if (key == null)
            return getForNullKey();
        //获取key,方法见(3)
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }

        (2)getForNullKey 方法:由put方法可知,key=null的元素都放在数组下标为0的位置

   private V getForNullKey() {
        //如果HashMap没有存储元素,返回null
        if (size == 0) {
            return null;
        }
        //取到数组下标为0的链表,遍历获取key为null元素返回
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

        (3)getEntry 方法

 final Entry<K, V> getEntry(Object key) {
        //如果HashMap没有存储元素,返回null
        if (size == 0) {
            return null;
        }
        //计算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        //通过key的hash值,计算数组存储的下标,循环遍历下标位置存储的链表,返回key相等的元素
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                return e;
            }
        }
        return null;
    }

      4.  关于jdk1.7与1.8 HashMap的差异

   由上所见,1.7解决hash冲突采用的是链表结构,而到了1.8,源码如下所示,插入元素时,首先判断插入的是否是树类型,如果不是,则判断冲突位置处的链表长度是否达到阈值(TREEIFY_THRESHOLD = 8),即链表长度达到8时,链表转为红黑树存储,未达到8时,则依旧采用链表结构。

猜你喜欢

转载自blog.csdn.net/qq_22200097/article/details/82791479