面试一次问一次,HashMap是该拿下了(一)

文章目录

  • 前言
  • 一、HashMap类图
  • 二、源码剖析
    • 1. HashMap(jdk1.7版本) - 此篇详解
    • 2. HashMap(jdk1.8版本)
    • 3. ConcurrentHashMap
  • ~~   码上福利


前言

业精于勤荒于嬉,行成于思毁于随;

在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,开始思考…

今天一起来聊一聊 HashMap集合,看到这里,笔者懂,大家莫慌,先来宝图镇楼 ~
面试一次问一次,HashMap是该拿下了(一)_第1张图片

咳咳… 对于屏幕前帅气的猿友们来说,HashMap… 张口就来,闭眼能写,但是呢,面试一问立马慌,自己阅读源码又隐隐觉得知其然不知其所以然;

那么…此时,笔者帅气的脸庞似有似无洋溢起一抹微笑,毕竟是查看过源码的猿,就是那么的豪横,话不多说,来吧,展示…



一、HashMap类图

面试一次问一次,HashMap是该拿下了(一)_第2张图片



二、源码剖析


1. HashMap(jdk1.7版本) - 此篇详解


大家都知道,jdk1.7版本底层数组+链表(单向链表),结合笔者的经验之谈,我觉得在分析HashMap集合具体操作源码前,有必要先了解下其底层链表结构,上源码…


  • 链表结构 - 单向链表

    /**
     * HashMap1.7中定义- 单向链表
     */
    static class Entry<K,V> implements Map.Entry<K,V> {
     
        // key值
        final K key;
        // value值
        V value;
        // 下一个节点
        Entry<K,V> next;
        // hash值
        int 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; }

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

        // 重写equals方法
        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;
        }

        // 重写hashCode方法
        public final int hashCode() {
      return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); }

        // 重写toString方法
        public final String toString() {
      return getKey() + "=" + getValue(); }

        // value被覆盖调用一次
        void recordAccess(HashMap<K,V> m) {
      }

        // todo:此此两方法主要作用于HashMap的子类实现,eg:linkedHashMap

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

如此如此,这般这般… 然而…这就是HashMap1.7版本定义的链表结构之单向链表…

每一个Entry节点包含四个属性:key表示当前节点key值;value表示当前节点value值,next节点表示当前节点下一个节点,如当前节点为链表末尾节点,则当前节点的next节点为null;hash表示当前节点key值通过算法计算出来的hash值;

抽象图解如下(其实笔者并不是很认同此图能形象的代表链表结构,但抽象理解还是可以的):

      单个Entry节点:
面试一次问一次,HashMap是该拿下了(一)_第3张图片

      单向链表图解:

面试一次问一次,HashMap是该拿下了(一)_第4张图片

      HashMap1.7版本底层 数组 + 单向链表 图解:

面试一次问一次,HashMap是该拿下了(一)_第5张图片

  • 构造函数

    // 数组默认初始容量 - 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 加载因子 - 默认值
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 加载因子
    final float loadFactor;

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

    // 扩容阈值(也表示hashMap底层数组实际存放元素大小)
    int threshold;

    /**
     * 无参构造
     */
    public HashMap() {
     
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 有参构造
     * @param initialCapacity:自定义初始容量
     * @param loadFactor:自定义
     */
    public HashMap(int initialCapacity, float loadFactor) {
     
        // 判断初始容量值有效性
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

        // 判断最大初始容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;

        // 判断加载因子有效性
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

        // 设置加载因子-默认0.75
        this.loadFactor = loadFactor;
        // 设置扩容阈值(构造初始化为16,第一次put为12)
        threshold = initialCapacity;

        // 模板方法-默认无实现
        init();
    }

    // 模板方法-设计模式:表示继承可拓展
    void init() {
     

    }
    

从源码中可以看出,构造只为相关参数(加载因子、扩容阈值)进行初始化;

其中需注意一点,我们都知道HashMap的扩容阈值为12,但在构造初始化的时候扩容阈值为16(知识点虽小,但却是细节);

那么此篇文章重点要来了,静下心来,开始思考…

  • put() - 添加元素方法

    // 数组默认值-空数组
    static final Entry<?,?>[] EMPTY_TABLE = {
     };

    // 底层数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    // HashMap元素个数
    transient int size;

    // 记录对HashMap操作次数
    transient int modCount;

    transient int hashSeed = 0;

    /**
     * 入口
     */
    public V put(K key, V value) {
     
    
    	// 1.第一次put元素
        // 数组为空进行参数初始化-表示第一次put元素
        if (table == EMPTY_TABLE) {
     
            // 数组初始化/参数初始化
            // 第一次put时,threshold经过构造方法赋值为16
            inflateTable(threshold);
        }

        // 2.添加key为null的元素
        if (key == null)
            return putForNullKey(value);

		// 3.添加key非null的元素
        // 计算hash值
        int hash = hash(key);
        // 计算数组对应下标值
        int i = indexFor(hash, table.length);

        // 遍历数组下标为i的链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
     
            Object k;
            // hash冲突 && key相同
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
     
                // 获取遍历节点元素值
                V oldValue = e.value;
                // 对value进行覆盖
                e.value = value;
                // value被覆盖时调用
                e.recordAccess(this);
                // 返回旧元素值
                return oldValue;
            }
        }

        // 操作次数++
        modCount++;
        // 添加Entry节点
        addEntry(hash, key, value, i);

        return null;
    }

    // 数组初始化/参数初始化
    private void inflateTable(int toSize) {
     
        // 计算初始容量
        // 第一次put时,返回值:16
        int capacity = roundUpToPowerOf2(toSize);

        // 计算扩容阈值:16 * 0.75 = 12
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 初始化长度为16的table数组
        table = new Entry[capacity];

        // 此方法不影响主要功能,咱跳过此方法(有兴趣的猿友们可自行研究哦~)
        initHashSeedAsNeeded(capacity);
    }

    // 用于返回大于等于最接近number的2的整数次幂
    private static int roundUpToPowerOf2(int number) {
     
        // 第一次put元素时: 16 >= 数组最大容量(1 << 30) ? (1 << 30) : (16 > 1) ? Integer.highestOneBit((16-1) << 1) : 1
        // Integer.highestOneBit((16-1) << 1) = Integer.highestOneBit(30) = 16
        return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

    // 添加key为null的元素 - 可以看出key为null时,存放在数组下标为0的位置
    private V putForNullKey(V value) {
     
        // 遍历数组下标为0的链表
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     
            if (e.key == null) {
     
                // 获取遍历节点元素值
                V oldValue = e.value;
                // 对value进行覆盖
                e.value = value;
                // value被覆盖时调用
                e.recordAccess(this);
                // 返回旧值
                return oldValue;
            }
        }
        // 操作次数++
        modCount++;
        // 添加Entry节点
        addEntry(0, null, value, 0);

        return null;
    }

    // 添加Entry节点
    void addEntry(int hash, K key, V value, int bucketIndex) {
     
        // map元素个数 > 扩容阈值 && 当前数组位置对应链表不为空
        if ((size >= threshold) && (null != table[bucketIndex])) {
     
            // 将源数组中的元素值散列至新数组
            resize(2 * table.length);
            // 计算hash值 - 重新计算
            hash = (null != key) ? hash(key) : 0;
            // 计算对应新数组下标位置
            bucketIndex = indexFor(hash, table.length);
        }

        // 添加Eentry节点
        createEntry(hash, key, value, bucketIndex);
    }

    // 将源数组中的元素值散列至新数组
    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];

        // 将源数组中的元素值散列至新数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 将新数组赋值至源数组
        table = newTable;
        // 重新计算扩容阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    // 获取当前Key对应hash值
    final int hash(Object k) {
     
        int h = hashSeed;
        if (0 != h && k instanceof String)
            return sun.misc.Hashing.stringHash32((String) k);

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    // 获取对应数组下标位置
    static int indexFor(int h, int length) {
     
        return h & (length-1);
    }

    // 添加Eentry节点
    void createEntry(int hash, K key, V value, int bucketIndex) {
     
        // 获取数组下标对应链表
        Entry<K,V> e = table[bucketIndex];
        // 向链表中添加节点
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        // HashMap元素个数++
        size++;
    }

    // 将源数组中元素散列至新数组中
    void transfer(Entry[] newTable, boolean rehash) {
     
        // 获取新数组长度
        int newCapacity = newTable.length;

        // 遍历源数组,将元素按照一定规则散列至新数组
        // 外循环:遍历数组
        for (Entry<K,V> e : table) {
     
            // 内循环:遍历数组位置对应链表
            while(null != e) {
     
                // 获取当前节点下一个节点
                Entry<K,V> next = e.next;
                if (rehash) {
     
                    // true:重新计算hash值
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                // 获取对应新数组的下标值
                int i = indexFor(e.hash, newCapacity);

                // 下面三步一定要连起来去思考:
                // **前提条件,2次循环都作用于新数组同一下标位置的情况:
                //   第一次循环时,newTable[i]为空,先赋值给当前遍历节点的下个节点,再将当前遍历节点赋值给对应新下标的新数组,最后继续循环
                //   第二次循环时,newTable[i]为上次(存入同一下标位置对应新数组的链表),然后赋值给当前遍历节点的下个节点(此节点实则为上一次遍历节点的下一个节点,
                //      从这里可以看出,HashMap1.7这里用的是头插法),再将此链表赋值给同一下标位置的新数组中,最后不为空继续循环;
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    

果不其然,相信大部分猿友硬着头皮跟一大坨代码硬钢之下,还是放弃了抵抗,来到了这里看笔者结论;

此时,笔者帅气的脸庞似有似无洋溢起一抹微笑,毕竟是查看过源码的猿;

其实呢,看源码也是有其讲究的,相信细心的猿友已从笔者源码注释看出些许问道,正如其所料,其实说白了,put()添加元素方法只做了三件事,下面我们拆解分析下:


  • 第一次put元素(当前为第一次添加元素时):
  1. 计算扩容阈值:
	threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // threshold = 16 * 0.75 = 12
  1. 初始化底层数组:
	table = new Entry[capacity]; // capacity = 16
  • 添加key为null的元素:
  1. 遍历数组下标为0的链表:

    ⑴ 如判断已存在 key=null 的节点,则覆盖其value值;

	   if (e.key == null) {
     
	       // 获取遍历节点元素值
	       V oldValue = e.value;
	       // 对value进行覆盖
	       e.value = value;
	       // value被覆盖时调用
	       e.recordAccess(this);
	       // 返回旧值
	       return oldValue;
	   }

        ⑵ 反之,则添加节点;
       // 操作次数++
       modCount++;
       
       // 添加Entry节点
       addEntry(0, null, value, 0);

  • 添加key非null的元素:
  1. 计算key键对应的hash值:

  2. 通过hash值计算对应数组下标存放位置;

  3. 遍历数组对应下标的链表(步骤2计算的下标):

    ⑴ 如判断hash值相等且key值相等,则覆盖其value值;

	if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
     
         // 获取遍历节点元素值
         V oldValue = e.value;
         // 对value进行覆盖
         e.value = value;
         // value被覆盖时调用
         e.recordAccess(this);
         // 返回旧元素值
         return oldValue;
     }

        ⑵ 反之,则添加节点;
       // 操作次数++
       modCount++;
       
       // 添加Entry节点
       addEntry(0, null, value, 0);

不知过了许久…

此时,笔者嘴角若隐若现一丝弧度微起,源码么,也不过如此…

咳咳… 请原谅笔者,毕竟从小到大无如此之成就感爆棚,猝不及防下狠狠地又装了一把…

我们言归正传,相信屏幕前的猿友经过笔者此分析,或多或少收获些许源码知识,至于其中如何判断key存在,如何进行value值覆盖,如何添加Entry节点,相信对于现在已然拿下put()方法框架思路的猿友来说,只是so easy的事情了,那么…此时…往上翻翻吧,趁着思路清晰,静下心来,参考着笔者源码注释,争取拿下其方法细节…

此时此刻,屏幕前拥有盛世美颜的你,给也同样拥有盛世美颜的暖男笔者,赏脸来个三连吧…笔者已迫不及待准备好么么哒,亲在…


  • get() - 获取元素方法

    /**
     * 入口
     */
    public V get(Object key) {
     
        // key为空时获取元素值
        if (key == null)
            return getForNullKey();

        // 获取key对应Entry链表
        Entry<K,V> entry = getEntry(key);

        // 返回对应元素值
        return null == entry ? null : entry.getValue();
    }

    // key为空获取元素值 - 可以看出key为null时,在下标为0的位置的数组获取
    private V getForNullKey() {
     
        // map元素个数为0时返回 null
        if (size == 0) {
     
            return null;
        }

        // 获取下标为0的链表
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     
            // 遍历key=null,返回对应元素值
            if (e.key == null)
                return e.value;
        }
        // 无->返回null
        return null;
    }

    // 获取key对应Entry链表
    final Entry<K,V> getEntry(Object key) {
     
        // map元素个数为0时返回 null
        if (size == 0) {
     
            return null;
        }

        // 计算hash
        int hash = (key == null) ? 0 : hash(key);
        // 1.计算对应数组下标值 2.遍历数组位置对应链表
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
     
            Object k;
            // hash相等 && key相等
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        // 无->返回null
        return null;
    }
 

相信对于屏幕前已拿下put()方法的你来说,获取元素方法,简直very so easy;

从源码中可以看出,获取元素时做了2件事:

  • 获取 key=null 的元素:
  1. 遍历数组下标为0的链表:

    ⑴ 如判断已存在 key=null 的节点,则返回其value值;

     for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     
         // 遍历key=null,返回对应元素值
         if (e.key == null)
             return e.value;
     }

        ⑵ 反之,则返回null;
  • 获取 key非null 的元素:
  1. 计算key键对应的hash值:

  2. 通过hash值计算对应数组下标存放位置;

  3. 遍历数组对应下标的链表(步骤2计算的下标):

    ⑴ 如判断hash值相等且key值相等,则返回其value值;

    for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
     
        Object k;
        // hash相等 && key相等
        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }

        ⑵ 反之,则返回 null;

笔者:当你翻过代码中最高的的一座山之后,剩下的只是一码平川;


  • remove() - 删除元素方法

    /**
     * 入口
     */
    public V remove(Object key) {
     
        // 获取key对应Entry链表
        Entry<K,V> e = removeEntryForKey(key);
        // 返回删除元素值
        return (e == null ? null : e.value);
    }

    // 获取删除元素对应Entry链表
    final Entry<K,V> removeEntryForKey(Object key) {
     
        // map元素个数为0时返回 null
        if (size == 0) {
     
            return null;
        }

        // 计算hash值
        int hash = (key == null) ? 0 : hash(key);
        // 计算对应数组下标位置
        int i = indexFor(hash, table.length);
        // 获取数组下标为i对应链表
        Entry<K,V> prev = table[i];
        // 链表第一次赋值
        Entry<K,V> e = prev;

        // 遍历此链表
        while (e != null) {
     
            // 获取当前节点下一个节点
            Entry<K,V> next = e.next;
            Object k;
            // hash相等 && key相等
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
     
                // 操作次数++
                modCount++;
                // map元素个数--
                size--;

                // 当前判断表示:循环此链表第一个节点
                if (prev == e)
                    // 直接将当前遍历(删除)节点的下一个节点赋值即可
                    table[i] = next;
                else
                    // 表示循环非此链表第一个节点
                    // 将上个节点的next节点设置为 当前遍历(删除)节点的下一个节点
                    prev.next = next;
                // 移除一个entry调用一次
                e.recordRemoval(this);
                // 返回删除节点
                return e;
            }
            // 将当前遍历(非删除)节点赋值给prev
            prev = e;
            // 将当前遍历(非删除)节点的下一个节点赋值给e(下一次遍历的节点)
            e = next;
        }

        // e!=null但无对应key -> 返回此链表最后一个Entry节点
        // e==null -> 返回null
        return e;
    }
    

相信屏幕前的猿友,此时此刻正干劲十足,越挫越勇,那么… 恭黑雷(恭喜你),拿下HashMap近在咫尺;

从源码中可以看出,删除元素实则就做了一件事,修改节点之间的引用;

注意,删除元素中唯一比较绕的就是此代码,结合笔者注释,注意细节即可:
面试一次问一次,HashMap是该拿下了(一)_第6张图片



  • HashMap(jdk1.7版本)总结:
  1. 底层为数组 + 链表(单向链表);
  2. 线程不安全;
  3. 数组初始容量为16;
  4. 扩容加载因子为0.75;
  5. 扩容阈值 a.构造之后为16,第一次put()方法后为12;
  6. 扩容死循环问题 - 笔者之后会另起一篇详解;
  7. 有modCount;



2. HashMap(jdk1.8版本)

面试一次问一次,HashMap是该拿下了之 HashMap1.8版本



3. ConcurrentHashMap

面试一次问一次,HashMap是该拿下了之 ConcurrentHashMap



~~   码上福利


大家好,我是猿医生:

在码农的大道上,唯有自己强才是真正的强者,求人不如求己,静下心来,扫码一起学习吧…
https://marketing.csdn.net/poster/145?utm_source=765669642

你可能感兴趣的:(集合源码系列,java)