【源码剖析】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讲解完毕!

posted @ 2020-11-22 20:10  在下右转,有何贵干  阅读(452)  评论(0编辑  收藏  举报