zoukankan      html  css  js  c++  java
  • 【源码剖析】HashMap1.7 详解

    shadowLogo

    在我们面试中,HashMap几乎是必问项,因为HashMap在工作学习中都十分重要,

    只有我们了解了其底层实现原理,才能更高效地使用它

    那么,在本篇博文中,本人就先来讲解下有关HashMap1.7的重要知识点:

    首先是 数据存储结构:

    数据存储结构:

    1.7HashMap数据结构

    从上图中,我们能够看出:

    在JDK1.7版本,HashMap主要是以 数组+链表 形式存储的


    那么,接下来,本人就来带同学们深究下源码:

    源码剖析:

    首先,本人先来介绍一个类 —— Entry类

    Entry类:

    为什么要介绍这个类呢?

    答曰:

    因为 JDK1.7版本 中,HashMap存储键值对,就是使用该类型


    Entry类 源码:

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;	// 存储 “键”
        V value;	// 存储 “值”
        Entry<K,V> next;	// 存储 “下一个节点”
        final int hash;	// 存储 当前键值对的“hash值”,便于之后的put操作
    
        /**
         * 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;
        }
    
    		/**
             * 比较顺序:
             * 1、目标对象的 类型
             * 2、目标对象的 “键”
             * 3、目标对象的 “值”
             * @param o 要比较的对象
             * @return
             */
        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 (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
    
        public final String toString() {
            return getKey() + "=" + getValue();
        }
    
        /**
         * (jdk未实现)
         * 每当调用HashMap中 已存在的键k 的put(k,v)覆盖条目中的值时,都会调用此方法。
         */
        void recordAccess(HashMap<K,V> m) {
        }
    
        /**
         * (jdk未实现)
         * 每当从表中删除条目时,都会调用此方法。
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }
    

    接下来,本人来介绍下 JDK1.7版本中的 HashMap类成员属性

    成员属性:

    在HashMap中,有以下 三个重要参数

    1. size (容量)
    2. loadFactor (负载因子)
    3. threshold (扩容阈值)

    容量 —— capacity:

    • 容量范围:必须是2次幂 且 小于最大容量(2的30次方)
    • 初始容量 = 哈希表创建时的容量
    • 默认容量 = 1<<4 = 2^4(十进制) =16
    /**
     * 默认初始容量,必须为2的幂。
     * (至于为何必须是2次幂,将在下文中的 初始化环节 进行讲解)
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    
    /**
     * 最大容量,如果两个构造函数都使用参数隐式指定了更高的值,则使用该容量。
     * 必须是两个<= 1 << 30的幂。
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    

    负载因子 —— loadFactor:

    负载因子

    • 意义:HashMap在其 扩容前 可达到大小的一种尺度
    • 加载因子越大、填满的元素越多:
      空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
    • 加载因子越小、填满的元素越少:
      空间利用率小、冲突的机会减小、查找效率高(链表不长)
    /**
     * 在构造函数中未指定时使用的负载因子。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    /**
     * 哈希表的负载因子。
     */
    final float loadFactor;
    

    扩容阈值 —— threshold:

    • 扩容阈值(threshold):当 哈希表的大小 大于等于 threshold 时,就会扩容哈希表(即 扩充HashMap的容量)
    • 扩容
      对哈希表进行resize操作(即 重建内部数据结构),从而哈希表将具有大约两倍的桶数
    • threshold = capacity * load factor
    /**
     * 映射容量的默认阈值,高于默认值时,散列度会降低,需要选择新的hash表
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
    
    /**
     * 要调整大小的下一个大小值 (capacity * load factor).
     */
    int threshold;
    
    

    其它成员属性:

    /**
     * 空数组,在数组进行初始化时会使用到,
     * 仅用于判断,不修改内容
     */
    static final Entry<?,?>[] EMPTY_TABLE = {};
    
    /**
     * 用于存储数据的数组,根据需要调整大小。
     * 长度必须始终为2的幂。
     * (至于为何必须是2次幂,将在下文中进行讲解)
     * HashMap的实现方式 = 拉链法,Entry数组上的每个元素本质上是一个单向链表
     */
    transient Entry[] table = (Entry<K,V>[]) EMPTY_TABLE;	// transient关键字: 实例化对象的该属性 不参加“序列化”
    
    /**
     * 此映射中包含的 键值对数(真实“键值对”数)
     */
    transient int size;	// transient关键字: 实例化对象的该属性 不参加“序列化”
    
    /**
     * 对该HashMap进行结构修改的次数结构修改是指更改HashMap中的映射次数或以其他方式修改其内部结构
     * (例如,重新哈希)的修改。
     * 此字段用于使HashMap的Collection-view上的迭代器快速失败。
     * (请参见ConcurrentModificationException)。
     */
    transient int modCount;
    

    相信许多同学在看完上述内容后,仍是对其中很多属性的意义不明确
    那么,下面本人就来通过一张图展示下 每个属性的意义
    属性的意义


    接下来,有了上述的铺垫,本人就来展示下 HashMap类核心方法源码
    核心api

    1. new HashMap()
    2. hashmap.put(key, value)
    3. hashmap.containsKey(key)
    4. hashmap.keySet()
    5. hashmap.get(key)
    6. hashmap.putAll(Map<? extends K, ? extends V> m);
    7. hashmap.remove(Object key);
    8. hashmap.containsValue(Object value);
    9. hashmap. keySet();
    10. hashmap.values();
    11. hashmap.clear();
    12. hashmap.size();
    13. hashmap.isEmpty();

    在我们使用 HashMap时,基本上都是通过如下顺序:

    1. 构造初始化
    2. put类填充
    3. 其它操作

    那么,本人就按照上面的顺序,来带同学们一一剖析:

    构造方法:

    JDK对于HashMap类,提供了如下四种 构造函数

    /**
     * 构造一个具有 指定“初始容量”和“负载因子” 的 “空HashMap”
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        // HashMap的最大容量只能是MAXIMUM_CAPACITY,哪怕传入的 > 最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
    
        this.loadFactor = loadFactor;
        
        // 设置 扩容阈值 = 初始容量
        // 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算
        threshold = initialCapacity;   
    
        init(); // 空方法,以便 子对象的扩展
    }
    
    /**
     * 构造一个具有 “指定初始容量” 和 默认负载因子(0.75)的空HashMap。
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    /**
     * 使用默认的初始容量(16)和默认的加载因子(0.75)构造一个空的HashMap。
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }
    
    /**
     * 使用默认的初始容量(16)和默认的加载因子(0.75)
     * 构造一个具有与指定Map相同的映射关系的新HashMap。
     * 
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }
    

    在看完上述的源码之后,相信同学们会发现:

    当我们执行完 构造函数 后,只是对 容量(capacity)加载因子(Load factor) 两个属性 进行了赋值,并没有对 table数组进行初始化

    实际上,真正初始化哈希表(table数组)是 在第1次添加键值对时,即第1次调用put()方法 时


    put()方法:

    /**
     * 将指定值与该映射中的指定键值对。
     * 如果该映射先前包含该键值对,则将替换旧值。
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        // 懒加载模式,每次调用put()方法都会先判断数组是否已经初始化了
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
    
        /*
                遍历 当前哈希表,查找 “目标键”是否存在:
                    若存在,则覆盖旧值,并将旧值返回
                    若不存在,则向 哈希表 中添加 新的键值对,并返回null
             */
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 若 hash值相等,则称这种情况为“哈希碰撞”,
            // 若发生 “哈希碰撞”,则通过 equals()方法来判断 两个key 是否 “一致”
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        modCount++;	// 修改一次 哈希表的容量,则使得modCount加一
        addEntry(hash, key, value, i);
        return null;
    }
    

    那么,本人根据上述的源码,来绘制一张 流程图

    流程图:

    put流程图

    在上图中,我们能够看到:

    put()方法的执行,主要是 如下步骤:

    1. 判断 哈希表(table数组) 是否 未初始化
    2. 判断 目标key 是否为 null
    3. 根据 目标key 生成 相应的hash值
    4. 根据 hash值哈希表(table数组) 的长度 ,生成 当前键值对所在 哈希表(table数组) 中的 下标

    那么,本人根据这张流程图,来逐一讲解下 put()方法 中,所运用到的 方法:

    inflateTable(threshold) 方法:

    private void inflateTable(int toSize) {
    	// 取容量为大于等于toSize的2的指数次幂,原因在后面讲解
        int capacity = roundUpToPowerOf2(toSize);
    	// 临界值最大只能取MAXIMUM_CAPACITY+1
    	// 如果未指定capacity和loadFactor,那么threshold=12
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        // 计算hashSeed,不超过MAXIMUM_CAPACITY就会一直保持为0,映射了最后一个属性
        initHashSeedAsNeeded(capacity);
    }
    

    在上述方法中,我们能够看到:

    还是调用了其它两个方法

    那么,本人现在来 讲解下 那两个方法:

    roundUpToPowerOf2(int number)方法:

    /**
     * 返回一个 比参数大的、最小的 2次幂
     * @param number 目标参数
     * @return 比参数大的、最小的 2次幂
     */
    private static int roundUpToPowerOf2(int number) {
        /*
            若 参数 超过了 最大容量,返回 最大容量
            否则,返回 比参数大的、最小的 2次幂
         */
        return number >= MAXIMUM_CAPACITY 
            ? MAXIMUM_CAPACITY 
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }
    

    继续解析 higestOneBit()方法:

    higestOneBit()方法:
    作用:

    返回 比所传参数小最大二次幂

    源码展示:
    /**
     * 返回一个 比所传参数小 的 最大二次幂
     * @param i 目标参数
     * @return 比所传参数小 的 最大二次幂
     */
    public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }
    
    原理解析:

    参数 为 int类型
    一个int有且仅有 32位
    按照 每一行代码调用执行,只要参数不为0

    随着每一步的 右移或等运算,会将 整个int的低16位全部转换为1
    最后再将计算结果执行如下代码:

    i - (i >>> 1)
    

    就会使得 仅原参数的最高位的1 保留下来,
    而正巧,每个2次幂数,都是 仅一位为1的int

    看完 higestOneBit()方法 的源码,相信很多同学和本人初学时一样,抱有如下疑问:

    为什么指定 长度 时,需要 二次幂 呢?

    那么,这个答案,将在下文中的 indexFor()源码讲解 中进行解释。


    initHashSeedAsNeeded(int num)方法:

    /**
     * 根据 参数 初始化 hashSeed
     * @param capacity 当前哈希表 容量
     * @return 
     */
    final boolean initHashSeedAsNeeded(int capacity) {
        //当我们初始化的时候hashSeed为0,0!=0 这时为false.
        boolean currentAltHashing = hashSeed != 0;
        //isBooted()这个方法里面返回了一个boolean值,我们看下面的代码
        boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
    

    (这个方法暂时不需要深究,在下文的总结段本人将详细讲解!)


    putForNullKey(V value)方法:

    /**
     * 键为null 的 put()方法
     * 若找到,则覆盖
     * 若没找到,则 使得modCount加1,并添加键为null、值为value 的键值对
     */
    private V putForNullKey(V value) {
        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;
    }
    

    hash(int h)方法:

    /**
     * 将补充哈希函数应用于给定的hashCode,以防止质量差的哈希函数。
     * 这很关键,因为HashMap使用2的幂的哈希表,否则哈希表在低位无差异时会遇到冲突。
     * 注意:空键始终映射到哈希0,因此索引为0。
     */
    static int hash(int h) {
        // 这个函数确保在 每个位元位置 上
        // 仅以 常数倍数 存在差异的哈希码 有一个 有限的冲突数(在 默认负载因子 下约为 8)
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    

    (这个方法 只是用来生成hash值,也不用深究)


    在上文的 inflateTable(threshold) 方法 中的 roundUpToPowerOf2(int number)方法 中的 higestOneBit()方法 的讲解中,

    本人讲到:

    roundUpToPowerOf2(int number)方法 的作用是 返回 比所传参数小最大二次幂

    那么,为什么要操作 2次幂 呢?

    答曰:

    请看 indexFor(int h, int length)方法

    indexFor(int h, int length)方法:

    /**
     * 返回哈希码h的索引
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }
    

    虽然上述的方法的源码 仅1行,

    但是让我们来细品下这行代码的作用:

    我们都知道:

    数组下标的范围 和 数组长度 的关系:
    $$
    0 <= 下标范围 < 数组长度
    $$
    而 h & (length-1) 的范围是: [0, length-1],恰好符合 数组下标数组长度 的关系

    但是,这又和本人上文中所讲解的 inflateTable()方法 时 初始化的长度必须是2次幂 有什么关系呢?

    答曰:

    由于我们要将 length-1 后,与所求得的 hash 进行 &运算

    是为了取得hash的后几位,且在length内

    那么,我们假设长度为16,则length-1的二进制表示为:

    1111

    这样,我们&运算的结果,就在 [0, length-1]中

    但是,这样的结果,要求的是 length-1的二进制表示为全1,也就是说:length的二进制必须有且只有一个1,即:2次幂

    相信看到上文的解释,同学们会惊叹于jdk开发者的天才脑洞!


    而最后的方法当然就是 加入新的键值对操作 —— addEntry(int hash, K key, V value, int bucketIndex)方法

    addEntry(int hash, K key, V value, int bucketIndex)方法:

    /**
     * 将具有指定键,值和哈希码的新条目添加到指定存储桶。
     * 如果合适,此方法负责调整表的大小。
     *
     * 子类重写此方法以更改put方法的行为。
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);	// “头插” 新节点
        if (size++ >= threshold) {
            resize(2 * table.length);
        	hash = (null != key) ? hash(key) : 0;  // 重新计算该Key对应的hash值
        	bucketIndex = indexFor(hash, table.length);  // 重新计算该Key对应的hash值的存储数组下标位置
        }
        
        createEntry(hash, key, value, bucketIndex);
    }
    
    
    

    可以看到:

    当我们加入一个新的键值对时,会执行如下步骤:

    1、将 哈希表目标下标(上述方法计算得到的) 中的元素(null或链表的表头) 取出

    2、将 当前元素 插入 取出的链表表头,并将 哈希表的目标下标元素 改为 当前元素

    3、计算 当前size 是否大于等于 扩容阈值,并使得 当前size+1

    4、若 当前size 到达 扩容阈值,则 扩容原哈希表的2倍

    那么,我们现在来看看上述方法所运用到的、同时也是容器类中非常重要的方法 —— resize()方法

    resize()方法:

    /**
     * 将此映射的内容重新映射到容量更大的新数组中。
     * 当此映射中的键数达到其阈值时,将自动调用此方法。
     * 如果当前容量为MAXIMUM_CAPACITY,则此方法不会调整map的大小,而是将阈值设置为Integer.MAX_VALUE。
     * 这具有防止将来通话的效果。
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {  // 若 旧容量 是 最大值,就不能再扩容了,只能改变“扩容阈值”
            threshold = Integer.MAX_VALUE;
            return;
        }
    
        /*
            若 旧容量 小于 最大值:
                创建新的数组空间(大小为所传参数),将原哈希表中的数据 转存到 新哈希表 中
                并 改变“扩容阈值” 为 新容量*负载因子 和 最大容量+1 中的 最小值
         */
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 将原哈希表中的数据 转存到 新哈希表 中
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    

    那么,我们再来看看 转存方法 —— transfer()方法:

    transfer(Entry[] newTable)方法:
    /**
     * 将所有条目从当前表传输到newTable
     */
    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;
                // 只有产生了新的hash表才需要重新计算hash值
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                /*
                	计算 当前节点 在 新哈希表 中的下标,
                	newCapacity = 原capacity*2,则indexFor()的结果为:
                	    oldIndex(原哈希的比最后一位&掉的位是0) 或 oldIndex + oldCapacity(原哈希的比最后一位&掉的位是1)
                 */
                int i = indexFor(e.hash, newCapacity);  // 计算结果为:i = oldIndex 或 i = oldIndex + oldCapacity
                /*
                    将 当前元素 “头插”入当前数组元素的链表
                 */
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    

    这个转存方法,是 导致HashMap线程不安全元凶!

    为什么呢?

    我们这样来思考:

    假设原哈希表有2个数组空间(仅假设,便于画图示意)

    old-table

    现在有两个线程同时运行到了resize()方法中,且都运行到了 transfer()方法

    第二个线程 仅运行到 Entry<K, V> next = e.next; 就失去了临界资源

    step1

    第一个线程 开始转存 原哈希表第一个哈希表空间

    根据上文的讲解,原哈希表中仅有的那条链表,仅能存储到新哈希表下标0下标2 的数组单元中

    我们假设 这条链表所有元素都存储到了 下标为2 的数组单元中

    step2

    这时,线程2开始运行,开始执行之后的代码

    但是,由于之前 线程2 执行过 Entry<K, V> next = e.next;

    因此,之后的执行,会是如下形式:

    step3

    继续一轮循环

    step4

    这时,当下一轮的 e.next = newTable[i];执行时, 就开始 "无限循环"了

    step5

    如上图所示,出现了 "长度为2循环链表"

    之后,两个指针就在 这两个节点中来回"游走",直至CPU耗尽!

    而这,就是 HashMap类 的 线程安全问题,

    它也有一个凶名赫赫 的外号 —— HashMap死锁

    那么,分析到这里,put()操作也就分析完毕了!


    接下来,其余方法,本人仅展示 源码 以及 略微讲解,

    (因为 集合类最重要的方法是 存储)

    get(Object key)方法:

    public V get(Object key) {
    	// 得到key=null的value值,可能为空
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
    
        return null == entry ? null : entry.getValue();
    }
    

    具体流程,如源码所示:

    get

    下面是 get()方法中用到的 两个方法:

    getForNullKey():

    /**
     * get()的卸载版本以查找空键。
     * 空键映射到索引0
     * 为了在两个最常用的操作(获取和放置)中提高性能,此空情况被拆分为单独的方法,但在其他条件中并入了条件。
     */
    private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    

    getEntry(Object key):

    /**
     * 返回与HashMap中的指定键关联的条目。
     * 如果HashMap不包含该键的映射,则返回null。
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
    	// 计算hash值
        int hash = (key == null) ? 0 : hash(key);
        // 遍历指定数组下标的链表,找到满足条件的Entry
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            // key可以是同一对象,也可以是不同的对象,只要equals比较成立即可
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
    

    remove(Object key):

    /**
     * 如果存在,则从此映射中删除指定键的映射。
     *
     * @param  key key whose mapping is to be removed from the map
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }
    

    removeEntryForKey(Object key):

    /**
     * 删除并返回与HashMap中的指定键关联的条目。
     * 如果HashMap不包含此键的映射,则返回null
     */
    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;
    	// 这里是最基本的链表删除进行的遍历
        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                // 删除使原table数组产生了变化,所以modCount要改变
                modCount++;
                size--;
                if (prev == e)
                	// 当数据为链表头节点时,只用略过头节点即可
                    table[i] = next;
                else
                	// 当数据不为链表头节点时,需将前一节点的next指向删除节点的后一节点
                    prev.next = next;
                // HashMap中没有真正实现,不用考虑
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
    	// 返回删除后的节点,e可能为null
        return e;
    }
    

    keySet():

    /**
     * 返回此映射中包含的键的Set视图。
     * 该集合由map支持,因此对map的更改会反映在集合中,反之亦然。
     * 如果在对集合进行迭代时修改了映射(通过迭代器自己的remove操作除外),则迭代的结果不确定。
     * 该集合支持元素删除,该元素通过Iterator.remove,Set.remove,removeAll,retainAll和clear操作从映射中删除相应的映射。
     * 它不支持add或addAll操作。
     */
    public Set<K> keySet() {
        // transient volatile Set<K> keySet = null;
    	// 这里的keySet和values一样都是AbstractMap<K,V>抽象类中的成员
        Set<K> ks = keySet;
        
        // 懒加载模式 初始化keyset
        return (ks != null ? ks : (keySet = new KeySet()));
    }
    
    private final class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            return HashMap.this.removeEntryForKey(o) != null;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    

    values():

    /**
     * 返回此映射中包含的值的Collection视图。
     * 集合由map支持,因此对map的更改会反映在集合中,反之亦然
     * 如果在对集合进行迭代时修改了map(通过迭代器自己的remove操作除外),则迭代的结果是不确定的。
     * 集合支持元素删除,该元素通过Iterator.remove,Collection.remove,removeAll,retainAll和clear操作从映射中删除相应的映射。
     * 它不支持add或addAll操作。
     */
    public Collection<V> values() {
    	// transient Collection<V> values = null;
    	// 这里的keySet和values一样都是AbstractMap<K,V>抽象类中的成员
        Collection<V> vs = values;
        // 懒加载模式 初始化values
        return (vs != null ? vs : (values = new Values()));
    }
    
    private final class Values extends AbstractCollection<V> {
        public Iterator<V> iterator() {
            return newValueIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsValue(o);
        }
        public void clear() {
            HashMap.this.clear();
        }
    }
    

    containsValue(Object value):

    /**
     * 如果此映射将一个或多个键映射到指定值,则返回true。
     *
     * @param value value whose presence in this map is to be tested
     * @return <tt>true</tt> if this map maps one or more keys to the
     *         specified value
     */
    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();
    
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }
    

    相信同学们在看玩上述的源码解析后,还存在部分疑惑
    那么,在剖析完源码后,本人来小结下 HashMap 的相关知识点:

    总结:

    为什么HashMap要扩容?

    可能会有同学有这样的问题:

    HashMap 存储数据 的 数据结构数组+链表
    那么就不会像 List 和 Set 一样,出现 “存储空间不足”问题
    (这里的存储空间是指 自己申请的空间不够用,并 不是内存存储空间,因为内存空间不足是无可避免的)
    为什么HashMap要扩容

    答曰:

    为了 缩短查询效率
    链表越长,我们查询时候,要遍历的节点数就越多,
    因此,当 扩容是为了 增加离散度缩短查询效率


    modCount属性 有什么用?

    答曰:

    在源码中,我们能够发现:
    HashMap中存储的元素改变 时,modCount 就会加一
    那么,回顾下本人《【JUC剖析】专栏总集篇》中所讲解的 乐观锁机制
    没错,modCount 就是 乐观锁的“版本号”
    modCount 表示了 HashMap的改变次数,是HashMap类创造者提供的一种 快速失败的容错机制

    而modCount主要用于HashMap的一个内部类:HashMap迭代器 —— HashIterator类 中:

    HashIterator类 源码:

    private abstract class HashIterator<E> implements Iterator<E> {	// 集成 “迭代器接口”
        Entry<K,V> next;        // next entry to return
        int expectedModCount;   // For fast-fail
        int index;              // current slot
        Entry<K,V> current;     // current entry
    
        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }
    
        public final boolean hasNext() {
            return next != null;
        }
    
        final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
    
            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }
    
        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }
    
    }
    

    可以看到:

    modCount 在 HashIterator类 中 充当了 乐观锁的作用

    除此之外,modCount可以说是没有其它用途了!


    initHashSeedAsNeeded()方法 有什么用?

    那么,本人再来展示下 initHashSeedAsNeeded()方法的源码:

    initHashSeedAsNeeded()方法 源码:

    /**
     * 根据 参数 初始化 hashSeed
     * @param capacity 当前哈希表 容量
     * @return 
     */
    final boolean initHashSeedAsNeeded(int capacity) {
        //当我们初始化的时候hashSeed为0,0!=0 这时为false.
        boolean currentAltHashing = hashSeed != 0;
        //isBooted()这个方法里面返回了一个boolean值,我们看下面的代码
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
    

    我们不妨倒着推导下:

    我们也能够看到:hashSeed属性 在之前一直是0
    因此,currentAltHashing变量 在次之前一直是1,
    useAltHashing变量 为1 的条件是 本类的类构造器是 Bootstrap ClassLoader当前哈希表容量 大于 Holder.ALTERNATIVE_HASHING_THRESHOLD
    Holder.ALTERNATIVE_HASHING_THRESHOLD的值为我们通过 启动参数jdk.map.althhashing.threshold 设置的
    (不相信的同学请自行查阅源码)
    而当 useAltHashing 为 1 时,switching 为 true
    只有当 switching 为 true时,才能改变hashSeed

    而当hashSeed修改后,继续运行调用其的父方法inflateTable()父方法put() 后的代码

    int hash = hash(key);
    

    中,会用到hashSeed属性

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

    总而言之:

    initHashSeedAsNeeded(方法的目的是改变hashSeed
    而 hashSeed 是为了 方便根据不同场合,使用 启动参数jdk.map.althhashing.threshold增加散列度


    那么,至此,HashMap1.7讲解完毕!

  • 相关阅读:
    利用webpack构建vue项目
    关于写毕业设计网页代码写后感
    用canvas属性写一个五角星哦
    css3瀑布流布局
    css3学习笔记,随时帮你记起遗忘的css3
    自己做得一个用于直观观察css3 transform属性中的rotate 3D效果
    第一次讨论——关于块级元素与行内元素的区别,浮动与清除浮动,定位,兼容性问题
    软件工程第一次作业
    自我介绍
    自我介绍
  • 原文地址:https://www.cnblogs.com/codderYouzg/p/14021076.html
Copyright © 2011-2022 走看看