【源码剖析】HashMap1.8 详解

shadow-logo

(温馨提示:由于上一篇博文 《【源码剖析】HashMap1.7 详解》的详细讲解,和本篇博文的主题在很大程度上是一致的,

因此本人在重复的情节可能会一笔带过,没有基础的同学请先学习上一篇博文!😉)

在上一篇博文 《【源码剖析】HashMap1.7 详解》中,本人从源码角度,详细介绍了在 JDK1.7版本 下的 HashMap类

那么,在本篇博文中,本人来讲解下JDK1.8版本的 HashMap类源码

首先是 数据存储结构:

数据存储结构:

1.8HashMap数据结构

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

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

HashMap1.7 相比,HashMap1.8的底层采用了红黑树的数据结构

此结构的使用,使得HashMap的查询方法的 时间复杂度O(n) 降低为 O(lgn)


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

源码剖析:

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

本人在 《【源码剖析】HashMap1.7 详解》中讲到过:

HashMap1.7中,键值对 使用 Entity类存储

那么,相应地,在HashMap1.8中,键值对 则以 Node类 存储

Node类 源码:

/**
 * 基本哈希键值对节点,用于大多数Entity
 * (有关TreeNode子类的信息,请参见下文;有关Entry子类的信息,请参见LinkedHashMap。)
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;	// 存储 当前键值对的“hash值”,便于之后的put操作
    final K key;	// 存储 “键”
    V value;	// 存储 “值”
    Node<K,V> next;	// 存储 “下一个节点”

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

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

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

至于Node类中,所引用到的 TreeNode类
请观看本人博文 —— 《【源码剖析】红黑树(HashMap1.8) 详解》


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

成员属性:

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

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

容量 —— capacity:

  • 容量范围:必须是2次幂 且 小于最大容量(2的30次方)
  • 初始容量 = 哈希表创建时的容量
  • 默认容量 = 1<<4 = 2^4(十进制) =16
/**
 * 默认 初始容量
 * (必须是2次幂)
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 默认 最大容量
 * (必须是2次幂,且 小于等于 1<<30)
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 最小 树化容量
 * (若 数组容量 < 64,即使链表长度达到树化阈值,也只扩容而非树化链表)
 * 这个比较容易理解,毕竟树化的时间和空间代价是高昂的
 * 为了避免进行 扩容、树形化选择 的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
 */
static final int MIN_TREEIFY_CAPACITY = 64;



负载因子 —— loadFactor:

负载因子

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

/**
 * 负载因子
 *
 * @serial
 */
final float loadFactor;

扩容阈值 —— threshold:

  • 扩容阈值(threshold):当 哈希表的大小 大于等于 threshold 时,就会扩容哈希表(即 扩充HashMap的容量)
  • 扩容
    对哈希表进行resize操作(即 重建内部数据结构),从而哈希表将具有大约两倍的桶数
  • threshold = capacity * load factor
/**
 * 链表 ——> 红黑树 的阈值,
 * 在存储数据时,当链表长度 >= 8时,将链表转换成红黑树
 * 根据泊松分布,实际测试出:在loadFactor=0.75时,出现一条链上有8个节点的概率为0.00000006
 * 因为转红黑树是代价高昂的所以在极端情况下再转化为树结构是值得的
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树 ——> 链表 的阈值,
 * 当树结点数 <= 6时,将红黑树转换成链表
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 扩容阈值
 *
 * @serial
 */
int threshold;

其它成员属性:

/**
 * 存储 键值对 的 哈希表
 * 该表在首次使用时初始化,并根据需要调整大小。 
 * 分配时,长度始终是 2次幂。
 * (在某些操作中,我们还允许长度为零,以允许使用当前不需要的引导机制。)
 */
transient Node<K,V>[] table;

/**
 * 保存缓存的entrySet(),
 * 而keySet()和values()则使用了AbstractMap的属性
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * map中的 键值对数量
 */
transient int size;

/**
 * 对该HashMap进行结构修改的次数结构修改是指更改HashMap中的映射次数或以其他方式修改其内部结构
 * (例如,重新哈希)的修改。
 * 此字段用于使HashMap的Collection-view上的迭代器快速失败。
 * (请参见ConcurrentModificationException)。
 */
transient int modCount;

相信看过本人上一篇博文 《【源码剖析】HashMap1.7 详解》的同学对于上述属性的意义基本明确了

接下来,本人就来展示下 HashMap类核心方法源码
核心api

在我们使用 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);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 构造一个具有指定 初始容量 和 默认负载因子(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; // all other fields defaulted
}

/**
 * 构造一个具有 与指定Map相同的键值对 的 新HashMap
 * 使用 默认负载因子(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.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

那么,本人在这里来介绍并讲解下 最后一个构造方法所用到的 tableSizeFor()方法putMapEntries()方法

tableSizeFor()方法:

/**
 * 返回 大于等于参数 的 最小2次幂
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法和 HashMap1.7 中的 roundUpToPowerOf2()方法 实现手段类似,

相信同学们都很清楚这个算法的原理,本人就不唠叨了!


putMapEntries()方法:

/**
 * 实现 Map.putAll 和 Map构造函数
 *
 * @param m the map
 * @param evict 第一次构造此映射时为false,否则为true(中继到afterNodeInsertion方法)。
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();	// s代表 目标哈希表的大小
    if (s > 0) {
        if (table == null) { // 若哈希表不存在,则创建一个
            float ft = ((float)s / loadFactor) + 1.0F;	// 计算 当前size 应该对应的capcacity
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);	// 计算 当前哈希表 应有的容量
            if (t > threshold)
                threshold = tableSizeFor(t);	// 将 当前阈值 设为 大于等于t 的 最小2次幂
        }
        
        /*
        	若 哈希表 不为 null,且 当前键值对数 > 计算后的阈值
        	则 扩容哈希表
         */
        else if (s > threshold)
            resize();
        
        /*
        	遍历 目标哈希表,将目标哈希表中的 键值对 转存到 当前哈希表 中
         */
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

这个方法的流程很简单:

  1. 判断 当前哈希表是否存在:
  1. 若 哈希表 不存在:

创建一个哈希表

  1. 若 哈希表 存在,且 当前键值对数 > 计算后的阈值:
    则 扩容哈希表
  1. 遍历 目标哈希表,将目标哈希表中的 键值对 转存到 当前哈希表 中

那么,本人再来重点介绍下 上述方法所调用的 resize()方法

为什么本人要 重点介绍呢?

因为在 之后的 put()方法 中的 treeifyBin()方法 中,就用到了 resize()方法

而且,本人在 《【源码剖析】HashMap1.7 详解》中讲到 resize()方法 调用的 transfer()方法 会造成 线程安全问题

HashMap1.8中,resize()方法也会造成线程安全问题

但是,这里要注意的是:

在HashMap8中,出现的 线程安全问题不是 死锁
而是 数据丢失,具体分析请看下文 总结区

resize()方法:

/**
 * 初始化或增加表大小。
 * 如果为空,则根据字段阈值中保持的初始容量目标进行分配。
 * 否则,因为我们使用的是 2次幂,
 * 所以每个bin中的元素必须保持相同的索引,或者在新表中以2次幂偏移。
 *
 * @return the table
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;	// 获取 旧表容量
    int oldThr = threshold;	// 获取 旧表阈值
    int newCap, newThr = 0;
    if (oldCap > 0) {
        /*
        	若 旧表容量 >= 最大容量:
        		设置 阈值 为 Integer.MAX_VALUE,并 返回旧表(无法扩容)
         */
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        /*
        	若 旧表容量 < 最大容量,且 新表容量 < 最大容量,且 旧表容量 >= 默认初始化容量(16),
        	则 新阈值  = 2 * 旧阈值
         */
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 阈值 翻倍
    }
    /*
    	若 旧表容量 = 0,且 旧表阈值 > 0,
    		则 新表容量 = 旧表阈值
    	若 旧表容量 = 0,且 旧表阈值 = 0(第一次创建哈希表,并非扩容),
    		则 新表容量 = 默认初始化容量,新表阈值 = 默认加载因子 * 默认初始化容量
     */
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    if (newThr == 0) {	// 若 新阈值 为 0,重新计算 新阈值(新表容量*负载因子)
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    /*
    	若 旧哈希表 不为空,则将 旧表数据 重hash分布到 新表
     */
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;	// 将 当前 [链表/红黑树] 的 [头/根]节点 赋值为null,便于 垃圾回收
                if (e.next == null)	// 若 当前节点链 只存在 一个节点,计算并存储 该节点
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)	// 若 当前节点链 为 红黑树,则进行 红黑树 的 重hash分布
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { //  若 当前节点链 为 链表,则进行 链表 的 重hash分布
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        /*
                        	若 当前节点hash & 旧表容量 为 0,则 当前节点在 新表中的下标不变:
                        		1、将 当前节点 “尾插”到 loTail链表中
                        		2、将 loHead链表 放到 新表的指定下标处
                        	若 当前节点hash & 旧表容量 为 1,则 当前节点在 新表中的下标 = 旧表中的下标 + 旧表容量:
                        		1、将 当前节点 “尾插”到 hiTail链表中
                        		2、将 hiHead链表 放到 新表的指定下标处
                         */
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

resize()方法实现逻辑 也很简单:

1、计算 新表容量新表阈值

2、生成 新表

3、将 旧表中的数据 转移到 新表中:

遍历整个旧表:

1、若 当前节点链 为 红黑树,则进行 红黑树 的 重hash分布

2、若 当前节点链 为 链表,则进行 链表 的 重hash分布:

若 当前节点hash & 旧表容量 为 0,则 当前节点在 新表中的下标不变:

  1. 将 当前节点 “尾插”到 loTail链表中
  2. 将 loHead链表 放到 新表的指定下标处

若 当前节点hash & 旧表容量 为 1,则 当前节点在 新表中的下标 = 旧表中的下标 + 旧表容量:

  1. 将 当前节点 “尾插”到 hiTail链表中
  2. 将 hiHead链表 放到 新表的指定下标处

那么,为什么 resize()方法 的使用,会产生 线程安全问题 呢?

答曰:

假设 现在有 两个线程 同时运行到了 if (oldTab != null)

线程1 转存了 一半链表线程1创建的 newTab

这时,线程1 失去 临界资源,线程2 开始工作

线程2 将 剩余的一半 中的 部分链表转存到了 线程2创建的 newTab

...

经过几次如上步骤的 线程的切换最终返回的只能是其中一个 newTab

这就导致 每个线程都会出现“数据丢失”的现象!


put()方法:

源码展示:

/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 *
 * @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) {
    return putVal(hash(key), key, value, false, true);
}

那么,本人现在来讲解下put()方法中所用到的 putVal()方法

putVal()方法:

/**
 * 实现 Map.put 和 相关方法
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent 如果为true,不要改变现有的价值
 * @param evict 如果为false,该表处于建造模式
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    /*
    	若 当前哈希表无效,则:
    		初始化 哈希表,并将 初始化后的哈希表长度 赋值给 n
     */
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*
    	根据 当前键,计算 当前键值对 应在的 哈希表下标:
    		若 当前哈希表下标的存储单元 为null:
    			根据当前键值对,创建一个新节点,并填充到 当前哈希表的制定下标 处
    		若 当前哈希表下标的存储单元 不为null:
    			1、若 目标节点的key已存在,且为 当前节点链的头节点,则:
    				覆盖当前旧值,并将旧值返回
    			2、若 [头/根]节点是 红黑树节点,则:
					将 当前节点 按照 红黑树节点插入规则 进行 插入操作
    			3、若 [头/根]节点是 链表节点,则:
    				顺着当前链表向下遍历,遍历的同时计数:
    					1、若 找到 目标键所在节点,结束遍历,覆盖当前旧值,并将旧值返回
    					2、若 未找到 目标键所在节点:
    						1、在 当前链表 “尾部” 根据 目标键值对 添加 一个新的链表节点
    						2、若 该链节点数 >= TREEIFY_THRESHOLD,树化当前链表,并结束遍历
     */
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))	// 若 目标节点的key已存在,且为 当前节点链的头节点
            e = p;
        else if (p instanceof TreeNode)	// 若 [头/根]节点是 红黑树节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {	// 若 [头/根]节点是 链表节点
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);	// 根据 当前哈希表 和 目标节点的hash,树化 当前链表
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;	// 键值对数发生改变,modCount加1
    
    /*
    	若 当前哈希表大小 > 阈值,则扩容
     */
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);	// 空方法,便于 子类的扩展
    return null;
}

现在,本人来展示下上述方法所用到的 newNode()方法

newNode()方法:

// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

那么,本人再来展示下上述方法所用到的 treeifyBin()方法

treeifyBin()方法:

/**
 * 除非表太小,否则替换给定哈希值的索引中bin中所有链接的节点,在这种情况下,将调整大小
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    /*
    	若 当前哈希表为null,或者 当前哈希表无效:
    		则 扩容当前哈希表
    	若 当前哈希表 有效:
    		1、根据 键值,构建“红黑树节点”
    		2、将 原链表的每一个节点,转换为 相应的TreeNode节点
    		3、将 当前哈希表 的 目标下标空间的值 改为 当前TreeNode链表 的 头节点
    		4、树化 当前TreeNode链表
     */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);	// 根据每一个“Node节点”,构造一个 “红黑树节点”
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

根据如上源码,我们能够得出,treeifyBin()方法流程如下:

1、若 当前哈希表为null,或者 当前哈希表无效:则 扩容当前哈希表
2.、若 当前哈希表 有效:

1、将 原链表的每一个节点,转换为 相应的TreeNode节点
2、将 当前哈希表 的 目标下标空间的值 改为 当前TreeNode链表 的 头节点
3、树化 当前TreeNode链表


那么,本人再来展示下 上述方法中 所调用的 replacementTreeNode()方法

replacementTreeNode()方法:
/**
 * 根据参数,构造一个 “红黑树节点”
 */
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

最后,本人来展示下 空方法 —— afterNodeInsertion()方法

afterNodeInsertion()方法:
// Callbacks to allow LinkedHashMap post-actions
void afterNodeInsertion(boolean evict) { }

那么,本人现在来 总结下putVal的执行流程

putVal的执行流程:

1、保证 当前哈希表有效:

若 当前哈希表无效,则:
初始化 哈希表,并将 初始化后的哈希表长度 赋值给 n

2、将 目标键值对 插入 当前哈希表 中:

根据 当前键,计算 当前键值对 应在的 哈希表下标:
若 当前哈希表下标的存储单元 为null:
根据当前键值对,创建一个新节点,并填充到 当前哈希表的制定下标 处
若 当前哈希表下标的存储单元 不为null:

1、若 目标节点的key已存在,且为 当前节点链的头节点,则:
覆盖当前旧值,并将旧值返回
2、若 [头/根]节点是 红黑树节点,则:
将 当前节点 按照 红黑树节点插入规则 进行 插入操作
3、若 [头/根]节点是 链表节点,则:
顺着当前链表向下遍历,遍历的同时计数:

1、若 找到 目标键所在节点,结束遍历,覆盖当前旧值,并将旧值返回
2、若 未找到 目标键所在节点:

1、在 当前链表 “尾部” 根据 目标键值对 添加 一个新的链表节点
2、若 该链节点数 >= TREEIFY_THRESHOLD,树化当前链表,并结束遍历

相信好多同学跟本人一样,不太想看文字,

那么,本人来通过一张图展示下 上述流程:

put流程


get()方法:

/**
 * 返回 指定键 所映射到的 值,若此映射不包含键的映射关系,则返回null。
 * 更正式地讲,如果此映射包含从键k到值v的映射,使得(key == null?k == null:key.equals(k)),则此方法返回v;否则返回null。 
 * (最多可以有一个这样的映射。)
 * 返回值null不一定表示该映射不包含该键的映射。
 * 映射也可能将键显式映射为null。 containsKey操作可用于区分这两种情况。
 */
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

那么,本人现在来展示下 get()方法 中所调用的 getNode()方法

getNode()方法:

/**
 * 实现 Map.get 和 相关方法
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    
    /*
    	若 当前哈希表 有效,且 存在键为key的键值对节点
     */
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        /*
        	首先判断 找到的(链表/红黑树) 的第一个节点:
        		若 first节点 的 hash 和 key 都满足要求,则返回 first节点
         */
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        /*
        	若 first 不是目标节点,则 遍历 first的后续节点(红黑树/链表)
         */
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)	// 若 first 为 红黑树节点,根据 键和hash 在 红黑树 中查找
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {	// 若 first 为 链表节点,根据 键 在 链表 中查找
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

那么,本人现在根据上述源码,来绘制一张流程图,以便加深同学们的理解:

get流程

相信同学们耐心看完 源码的注释,再结合 流程图,一定会透彻理解 HashMap1.8get()方法


remove()方法:

/**
 * 如果存在,则从此映射中删除指定键的映射
 *
 * @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) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

那么,接下来,本人来展示下 remove()方法 中调用的 removeNode()方法 吧:

removeNode()方法:

/**
 * 实现 Map.remove 和 相关方法
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to match if matchValue, else ignored
 * @param matchValue 如果为true,则仅在值相等时删除,键值对都必须相等
 * @param movable 如果为false,则在删除时不移动其他节点
 * @return the node, or null if none
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    /*
    	若 当前哈希表 不为null、 长度不为0,且 目标key的映射存在:
    		先查找,再删除
     */
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)	// 若 p节点 为 红黑树节点,则根据 红黑树的查找原则 进行 查找
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {	// 若 p节点 为 链表节点,则根据 链表查找原则,进行 查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        /*
        	若找到 目标键对应的键值对,
        	并且 根据参数matchValue,可以选择是否比较 value是否相同
         */
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)	// 若 目标节点 为 红黑树节点,按照 红黑树的删除原则 进行删除
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)	// 若 目标节点 为 链表节点 的 第一个节点,按照 链表的删除原则 进行删除
                tab[index] = node.next;
            else	// 若 目标节点 为 链表节点 的 非首节点,按照 链表的删除原则 进行删除
                p.next = node.next;
            ++modCount;	// 删除成功,改变 modCount,“快速失败的容错机制”
            --size;	// 删除成功,改变容量
            afterNodeRemoval(node);	// 空方法,便于子类的扩展
            return node;	// 将删除的节点返回
        }
    }
    return null;	// 若 map 中不存在 指定的键值对,则 返回null
}

从源码中,我们能够看到:

删除一个节点 的 大体流程 为:

  1. 查找 目标键对应的节点 是否存在
  2. 原哈希表删除目标节点

那么,本人再来展示下 removeNode()方法 中调用的 afterNodeRemoval()方法

afterNodeRemoval()方法:

// Callbacks to allow LinkedHashMap post-actions
void afterNodeRemoval(Node<K,V> p) { }

可以看到:

afterNodeRemoval()方法 是一个 空方法

这正是 HashMap作者 提供给我们,便于子类的扩展


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
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new AbstractSet<K>() {
            public Iterator<K> iterator() {
                return new Iterator<K>() {
                    private Iterator<Entry<K,V>> i = entrySet().iterator();

                    public boolean hasNext() {
                        return i.hasNext();
                    }

                    public K next() {
                        return i.next().getKey();
                    }

                    public void remove() {
                        i.remove();
                    }
                };
            }

            public int size() {
                return AbstractMap.this.size();
            }

            public boolean isEmpty() {
                return AbstractMap.this.isEmpty();
            }

            public void clear() {
                AbstractMap.this.clear();
            }

            public boolean contains(Object k) {
                return AbstractMap.this.containsKey(k);
            }
        };
        keySet = ks;
    }
    return ks;
}

values()方法:

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

final class Values extends AbstractCollection<V> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<V> iterator()     { return new ValueIterator(); }
    public final boolean contains(Object o) { return containsValue(o); }
    public final Spliterator<V> spliterator() {
        return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

containsKey()方法:

/**
 * 如果此映射将一个或多个键映射到指定值,则返回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 containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

从源码中,我们可以看到:

HashMap1.8containsKey()方法 处理手段也很简单:

根据 目标键,查询 相应的映射:

相应映射不为null,则 返回true

反之,则 返回false


那么,讲了这么多,
现在本人来总结几个注意点:

总结:

在上文中所提到的 transfer()方法 会发生的 线程安全问题 是什么呢?

线程安全问题:

我们还是假设存在 两个线程 t1t2

  1. 当 t1转存数据 一半时,失去临界资源
  2. t2继续转存剩下一半数据
  3. 交替往复,直至两个线程都return,
    这时,两个线程transfer后的数组都会有丢失!

之前有粉丝私信博主,问到了这样的问题:
粉丝提问

能不能讲解下 TreeNode类?

好嘛,不当人了😡
强杀
请观看本人博文 —— 《【源码剖析】红黑树 与 TreeNode》
卑微博主在线读源码... ...


posted @ 2020-11-25 13:26  在下右转,有何贵干  阅读(544)  评论(0编辑  收藏  举报