HashMap源码解析及常见问题

本文详细解析了HashMap的底层实现,包括其数据结构、数组长度的确定、扩容机制、元素插入过程及红黑树的应用。探讨了不同JDK版本间的差异,如JDK1.7与1.8在扩容和插入元素时的不同策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

主要方法解析
package cn.iyhome.hashmap;


import java.io.Serializable;
import java.util.*;

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

    //(1)初始化变量

    //初始化容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

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

    //默认装载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //装载因子
    final float loadFactor;

    //下次扩容阈值
    int threshold;

    //修改次数
    transient int modCount;

    //空表
    static final Map.Entry<?, ?>[] EMPTY_TABLE = {};

    //数组表,大小总是power of two
    transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;

    //?
    private transient Set<Map.Entry<K,V>> entrySet = null;

    //map大小
    transient int size;

    //(2)构造方法
    public MyHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);

    }

    public MyHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public MyHashMap(int initialCapacity, float loadFactor) {
        //初始值小于0:抛非法参数异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("...");
        //初始值大于最大值,令初始值为最大值
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //装载因子异常小于0或者不是浮点数,抛非法参数异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("...");
        //初始化参数装载因子
        this.loadFactor = loadFactor;
        //tableSizeFor:Returns a power of two size for the given target capacity.
        //jdk1.8 -> this.threshold = tableSizeFor(initialCapacity);
        this.threshold = initialCapacity;
    }

    //(3)jdk1.7 inflateTable方法

    /**
     * 初始化数组表
     *
     * @param toSize:默认值大小或用户在构造函数中设定的初始大小
     */
    private void inflateTable(int toSize) {
        //Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //初始化大小
        table = new Entry[capacity];
    }

    /**
     * Find a power of 2 >= toSize
     *
     * @param number 默认值大小或用户在构造函数中设定的初始大小
     * @return 修正后的值
     */
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

    //(4)jdk1.7 put方法
    public V put(K key, V value) {
        //如果数组为空
        if (table == EMPTY_TABLE)
            //初始化table数组
            inflateTable(threshold);
        //如果key为null
        if (key == null)
            return putForNullKey(value);
        //对key取hash值	//hash():hashSeed ^=k.hashcode(),再对hashSeed做了四次异或和右移4位运算,保证hash值足够分散,减少碰撞
        int hash = hash(key);
        //indexFor()方法求出hash值对应的数组下标
        int index = indexFor(hash, table.length);


        //table[index]:链表的头结点;遍历链表
        for (Entry<K, V> e = table[index]; e != null; e = e.next) {
            Object k;
            //如果key值重复,则覆盖value,返回旧value
            if (hash == hash(key) && (((k = e.key) == key) || (key.equals(k)))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
            //key不重复
        }

        modCount++;
        addEntry(hash, key, value, index);
        return null;
    }



    //(5)自定义hash方法
    final int hash(Object k) {
        //源码中:令hashSeed=k.hashcode(),再对hashSeed做了四次右移和异或运算,让高位也参与了运算,保证hash值足够分散,减少碰撞
        int h = k.hashCode() % table.length;
        return h;
    }

    //(6)jdk1.7 indexFor方法

    /**
     * 返回hash值h对应的数组下标
     *
     * @param h      hash()返回的哈希值
     * @param length 数组表的长度
     * @return
     */
    static int indexFor(int h, int length) {
        /*
         * 1.为什么要使用 h & (length-1) ?
         * param: hash & length
         * 假设:hash = 85 length = 16
         * DEC: 85 & 16 = 16
         * BIN:   0101 0101  hash
         *      & 0001 0000  length
         *      = 0001 0000  16
         * 此时,如果hash的低4位无论如何变,结果都是 0001 0000,因为length的低4位全是0,这种情况下计算出的数组下标超出了数组表的大小范围
         * 如果将length-1 即 length(15) = 0000 1111,此时
         * BIN:  0101 0101   hash
         *      &0000 1111   length
         *      =0000 0101   5
         * 最终的值随着hash的低4位变化而变化,而hash的低4位取值范围为 0000 - 1111 即 0-15,正符合数组表的下标长度
         * 所以:这就是为什么table无论是在初始化或者扩容的时候,自始至终都是2的次方大小
         * 2.为什么不用 % 取余来做? & 与操作效率高
         * */
        return h & (length - 1);
    }

    //(7)jdk1.7 putForNullKey方法

    /**
     * @param value 值
     * @return oldValue
     */
    private V putForNullKey(V value) {
        //遍历table[0]为头结点的链表
        for (Entry<K, V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                //更新值
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

    /**
     * 添加一个新节点到对应的数组下标所在的链表,并在达到条件的时候进行扩容
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果大小超出阈值且要添加的位置已经存在值,则进行2倍扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //扩容
            resize(2 * table.length);
            //key是否为null,null则数组下标为0,否则取hash
            hash = (null != key) ? hash(key) : 0;
            //再根据hash & length-1 ,获取到数组下标
            bucketIndex = indexFor(hash, table.length);
        }

        //添加节点位头结点
        createEntry(hash, key, value, bucketIndex);
    }

    /**
     * 达到扩容条件后,对map中的key重新做hash取值
     * 如果当前大小已经最大值,将不再扩容,但会将threshold的值设置为Integer.MAX_VALUE 1<<32 -1
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity 必须是2的次方且大于之前的大小,除非已经是最大值1<<30).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //transfer(newTable, initHashSeedAsNeeded(newCapacity));
        transfer(newTable);
        table = newTable;
        threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 将扩容前的节点复制到新集合
     *
     * @param newTable
     */
    private void transfer(Entry[] newTable) {
        int newCapacity = newTable.length;
        //遍历数组
        for (Entry<K, V> entry : table) {
            //遍历链表
            while (null != entry) {
                //存储好entry的下一个节点
                Entry<K, V> next = entry.next;
                //重新获取hash值
                entry.hash = null == entry.key ? 0 : hash(entry.key);
                //根据新hash值计算在newTable的下标
                int index = indexFor(entry.hash, newCapacity);

                //链表尾插法
                //如果newTable[i] == null,则直接将entry值覆盖newTable[i]
                //如果 != null,则先将entry.next指向已经存在的值(即newTable[i],即链表头),然后将新节点置为链表头
                entry.next = newTable[index];
                newTable[index] = entry;

                //将entry的next继续遍历
                entry = next;
            }
        }
    }

    /**
     * 删除元素
     *
     * @param key
     * @return
     */
    public V remove(Object key) {
        Entry<K, V> e = removeEntryForKey(key);
        return null == e ? null : e.value;
    }

    /**
     * 根据key移除Entry节点
     *
     * @param key
     * @return
     */
    private Entry<K, V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        //根据key获取数组下标
        int hash = null == key ? 0 : hash(key);
        int index = indexFor(hash, table.length);

        //设置节点,使用链表的删除方法(指针指向)
        Entry<K, V> prev = table[index];
        Entry<K, V> entry = prev;

        while (null != entry) {
            Entry<K, V> next = entry.next;
            Object k;
            //找到key对应的节点entry
            if (entry.hash == hash && ((k = entry.key) == key || (key.equals(k)))) {
                modCount++;
                size--;
                //如果prev==entry,说明是第一个节点即头结点
                if (prev == entry)
                    //直接将第二个节点作为头结点即可
                    table[index] = next;
                else
                    //删除中间节点,将相对的第三个节点作为相对的第一个节点的下一个节点
                    prev.next = next;
                entry.recordAccess(this);
                return entry;
            }
            //如果遍历的第N个节点与key不匹配,则继续遍历链表
            //将当前节点作为前一个节点
            prev = entry;
            //将下一个节点置为当前结点,继续遍历
            entry = next;
        }
        //若null == entry
        return entry;
    }


    // These methods are used when serializing HashSets
    int capacity() {
        return table.length;
    }

    float loadFactor() {
        return loadFactor;
    }

    /**
     * 清空集合
     */
    public void clear() {
        modCount++;
        //将数组所有对象引用置为null
        Arrays.fill(table, null);
        size = 0;
    }

    @Override
    public Set<Map.Entry<K, V>> entrySet() {
        return null;
    }

    /*public Set<Map.Entry<K,V>> entrySet() {
        return entrySet0();
    }

    private Set<Map.Entry<K,V>> entrySet0() {
        Set<Map.Entry<K,V>> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }*/

    /**
     * 创建一个节点
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        //记录链表头结点
        Entry<K, V> e = table[bucketIndex];
        //将头结点作为新节点的下一个节点,并将新节点放置在数组上
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //大小+1
        size++;
    }

    /**
     * 返回大小
     *
     * @return
     */
    public int size() {
        return size;
    }

    /**
     * 判断集合是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * getValue
     *
     * @param key
     * @return key对应的Value
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K, V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    /**
     * 根据key获取Entry对象
     *
     * @param key
     * @return
     */
    final Entry<K, V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //取key的hash
        int hash = null == key ? 0 : hash(key);

        //遍历查找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 || (k != null && key.equals(k))))
                return e;
        }
        return null;
    }

    /**
     * 是否包含key的value
     *
     * @param key
     * @return
     */
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    /**
     * get value when the key is null
     *
     * @return entry.value
     */
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K, V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    static class Entry<K, V> implements Map.Entry<K, V> {
        final K key;
        V value;
        Entry<K, V> next;
        int hash;

        /**
         * Creates new entry.
         */
        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;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        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;
        }

        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

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

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(MyHashMap<K, V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(MyHashMap<K, V> m) {
        }
    }



}



常见问题

1.HashMap底层的数据结构

JDK1.7以前HashMap底层是 数组+链表,jdk1.8使用的 数组+链表+红黑树

2.数组元素的数据类型

数组的类型是 HashMap.Entry<K,V> implement Map.Entry<K,V>,
HashMap.Entry<K,V>主要是维护K,V映射关系,Map.Entry维护了<K,V,next>

3.HashMap中数组的长度

数组的长度一直为2的次方.
如果在构造函数中定义的长度不是2的次方?
jdk1.7中,会在put方法中,如果数组是空,会调用inflateTable方法中的roundUpToPowerOf2()方法修正数组的长度,并计算下次扩容的阈值(数组长度*装载因子);
jdk1.8中,是在构造方法中就调用tableSizeFor()方法,将长度修正为2的次方

4.HashMap什么时候扩容?

put数据的时候,调用addEntry方法.当达到阈值threshold&&table[index]!=null的时候
源码为:if((size>=threshold)&&(null!=talble[indext]))

5.put元素的时候,index是如何计算的?

(1)如果key是null,固定位置index=0(注意:并不是index=0的位置全是key=null的数据)\
(2)第一步,通过hash(key)[四次异或和右移运算]取的一个较为分散的hash值;
   第二步,通过hash & length-1计算出index的值
   		为何是hash & length-1?第一部分源码分析有讲解

6.key重复的判断标准

key.hash == hash && (key.equal(k)||(k = e.key) == key)
即hash值相同和(key的地址相同或者equals相等)

7.集合扩容的是什么?

是数组+链表中,数组的大小.所以源码中初始值2<<4也是数组的大小,
而不是集合中所有元素的个数,即size

8.JDK1.7HashMap扩容死循环问题?

在多线程并发访问触发扩容后,可能会导致entry和next entry相互引用,导致死循环

9.JDK1.8 HashMap什么时候扩容?

第一种:链表的个数达到8个,并且table.length<64,那么会扩容
第二种:size >= threshold并且talbe[index]!=null

10.HashMap插入元素时,1.7和1.8有什么区别?

JDK1.7是头插法(所以会导致元素顺序颠倒),JDK1.8是尾插法

11.JDK.18中HashMap什么时候用到红黑树?

链表超过8个且table.length>64的时候树化,当树的节点小于6个,就会反树化。

12.如何解决HashMap的并发扩容导致的死循环问题?

1.整个方法加锁,这也是hashtable采用的方式 
2.分段锁(segment),currentHashMap采用的方式.
	并发级别默认16,也就是段数. 每个段就是一个Segment对象(每个Segment就是一个小的加了锁的HashMap),多个Segment就组成了一个数组.
	所以currentHashMap的put方法分为两步:第一步,通过key.hashCode找到Segment[index],再到小的HashMap中找到index

13.ConcurrentHash是如何控制Segment.size一定是2的次方的?

构造函数中有while循环ssize<<=1操作

14.JDK1.8扩容的迁移机制?

分为链表迁移和红黑树迁移.链表迁移使用了4个指针代替了rehash
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值