Java HashMap

HashMap 是 Map 接口中基于哈希表的非同步实现, 自身也可以自动扩容。使用时可以通过 key 快速定位到对应的 value。key 和 value 同时可以都为 null。

1 HashMap 的结构定义

JDK1.8 对 HashMap 进行了比较大的优化, 底层实现由之前的 “数组 + 链表” 改为 “数组 + 链表 + 红黑树”。
在链表的长度大于等于 8 并且数组的长度大于等于 64 时, 将对应的链表转为红黑树 (本文不涉及红黑树部分的分析, 涉及到时, 只会提一下, 然后跳过)。

首先数组是整个数据的存储真正实体, 数组中的存储的数据是链表或者红黑树, 当链表的长度达到了条件, 就变成了红黑树, 大体的情况如下:
Java HashMap_第1张图片

1.1 数组的定义

public class HashMap<K,V>  {

    transient Node<K,V>[] table;
}

从上面的代码就是 HashMap 中数据存储的地方: 数组。 数组存储的数据类型为 Node, 这个 Node 是链表的定义。
而 红黑树的定义是继承了 Node, 所以通过向上转型的方式, 就能通过一个 Node 的类型表示链表和红黑树。

1.2 链表的定义

public class HashMap<K,V> {

    /**
	 * 链表定义
     * Map.Entry 行为接口, 定义了一堆操作方法, 比如 getValue, setValue 等
	 */
    static class Node<K,V> implements Map.Entry<K,V> {

        // 当前节点的 hash 值
        final int hash;

        // 当前节点的 key
        final K key;

        // 当前节点的 value
        V value;

        // 下一个节点, 这个属性决定了 Node 为链表
        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;
        }

        /**
         * 计算当前节点 hashCode
         */
        public final int hashCode() {
        	// Objects.hashCode 本质就是调用对象的 o.hashCode()
        	// 当前节点的 hashCode 值 等于 key 的 hashCode  异或 value 的 hashCode 值
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        /**
         * 节点比较
         */
        public final boolean equals(Object o) {

            if (o == this)
                return true;

            // 都是 Map.Entry 节点
            if (o instanceof Map.Entry) {

                Map.Entry<?,?> e = (Map.Entry<?,?>)o;

                // Objects.equals 会先比较一下 2 者的内存地址, 一样直接返回 true
                // 不一样, 调用 key.equals(e.getKey) 进行比较
                if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))
                    return true;
            }

            return false;
        }

    }
}

1.3 红黑树的定义


public class HashMap<K, V> {

    // 继承了 LinkedHashMap.Entry, Entry 继承了 HashMap.Node  所以 TreeNode 具有 链表的特点

    /** 
     * 红黑树的定义
     * LinkedHashMap.Entry 继承了 HashMap.Node 节点, 所以 TreeNode 是 Node 的子类, 也具备链表的特点
     */    
	static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {

        /**
         * 红黑树的根节点
         */
        TreeNode<K,V> parent;
        
        /**
         * 当前节点的左节点
         */
        TreeNode<K,V> left;

        /**
         * 当前节点的右节点
         */
        TreeNode<K,V> right;

        /**
         * 删除后需要解决连接的节点
         */
        TreeNode<K,V> prev;  

        /**
         * 是否为红色节点
         */
        boolean red;

        // ... 后面 省略 红黑树的操作
    }
}

2 HashMap 中的几个重要属性

public class HashMap<K,V> {

    transient Node<K,V>[] table;

    transient int size;

    transient int modCount;

    final float loadFactor;

    private int threshold;

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

    static final int MIN_TREEIFY_CAPACITY = 64;
}

2.1 table

transient Node<K,V>[] table;

HashMap 中存储数据的数组

2.2 size

transient int size;

HashMap 中已经存储的数据个数

2.3 modCount

transient int modCount;

HashMap 已经被修改了多少次, 用于支撑 fail-fast 机制

2.4 loadFactor

final float loadFactor;

负载因子: 默认等于 DEFAULT_LOAD_FACTOR = 0.75f;
作用: 一般情况都是在容器满了才会进行扩容, 但是在 HashMap 中, 数据量达到了数组的长度 * 负载因子的值, 就会进行扩容了。
原因: HashMap 在数组中插入一个数据, 是先通过一个 hash 方法转换为一个 hash 值, 通过这个 hash 值计算得到存储在数组的位置, 通过 hash 计算, 就可能存在 hash 冲突。
数据越密集, 冲突的可能性越大, 所以 HashMap 中的数组是不会完成存满的, 通过空留一部分, 减少冲突等。

负载因子默认值为 0.75 的原因: 太大冲突可能性变大, 太小浪费了空间, 同时会导致数组扩容等耗时操作。
所以 0.75 应该是一个经验值的估算, 或者是因为 HashMap的数组长度为 2^n, 乘以 0.75, 能获得一个整数。

2.5 threshold

private int threshold;

当前数组的阈值, 即数组实际应该放多少数据

2.6 几个常量

// 1 左移多少位, 就是相当于 2 的 多少次方, 这里就是 2^4 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

声明 HashMap, 不指定容量时, 默认为 16。

// 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

HashMap 最大容量, 容量必须是 2 的 n 次方, 涉及到通过 key 的 hash 值定位到数组的位置中的一个配合。
HashMap 中数组的容量设定为 2 的 n 次方具体有 2 个作用:

第一, 得到了 key 对应的 hash 后, 如何通过 hash 定位到数组中的哪一个位置呢?

最简单的方式就是取模了吧, 通过 hash 值模于当前数组的长度, 得到的就是当前 hash 对应的数组位置。
但是取模的效率不太好, 那么有什么好的优化方式吗?

在数学上, a, b 都是正整数的情况下, a % b, 在 b 是 2 的 n 次方下, a % b = a & (b - 1)
位运算比正常的算术运算快, 那么将 HashMap 的容量设置为 2 的 n 次方, 就可以通过与运算达到取模的效果。
所以 HashMap 中的数组容量必须为 2 的 n 次方。

第二个作用在后面会分析。

static final int TREEIFY_THRESHOLD = 8;

HashMap 中链表变为红黑树的长度配置, 链表长度达到了 8 满足了链表变为红黑树的条件之一。

static final int UNTREEIFY_THRESHOLD = 6;

HashMap 中红黑树重新变为链表的长度配置, 红黑树的节点个数达到了 6, 满足了红黑树变为链表的条件之一。

static final int MIN_TREEIFY_CAPACITY = 64;

HashMap 中链表变为红黑树的的另外一个条件, 当前数组的长度达到了 64。

3 HashMap 的构造方法

3.1 无参的构造函数


public HashMap() {
    // 设置负载因子为 0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

3.2 指定初始容量 (和负载因子) 的构造函数

public HashMap(int initialCapacity) {

    // 调用到自身的指定容量 和负载因子的构造函数, 传入的负载因子为默认值 0.75
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/** 
 * 指定 初始容量 和 负载因子  
 */
public HashMap(int initialCapacity, float loadFactor) {

    // 指定的容量小于 0, 格式不正确
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

    // 指定的容量超过了最大容量, 设置为支持的最大容量值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    // 格式不正确 NaN: Not-a-Number,  isNaN(arg) => arg 不是数字, 返回true
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);    

    // 负载因子直接等于输入的值
    this.loadFactor = loadFactor;
    // tableSizeFor 这个方法会对输入的值, 转换为第一个大于或等于输入值的 2 的 n 次方的数, 比如: 输入 3 得到 4, 输入 4 得到 4, 输入 5 得到 8
    // 因为 HashMap 的容量需要是 2 的 n 次方, 通过 tableSizeFor 将用户输入的不是 2 的 n 次方的数, 进行修正
    
    // threshold 这个属性上面说过是用来存储 HashMap 的实际容量 (声明的数组容量 * 负载因子), 但是这里直接赋值的是 2 的 n 次方, 错了吗?
    // 实际上没错的, threshold 就是用来存储实际的容量的, 这里只是把计算出来的容量临时存在这个变量, 在第一次进行存放数据时, 进行修正为实际容量的
    this.threshold = tableSizeFor(initialCapacity);
        
}

/**
 * 计算数组容量
 */
static final int tableSizeFor(int cap) {
    // 此次减 1 的原因: 让后面的处理结果变为 n 是第一个大于等于 cap 的 2 的 n 次方的数
    // 如果这里不减 1 的话, 刚好传进来的数是 2 的 n 次方, 经过下面几步的处理会变成 2 的 n+1 次方的数
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    // 做一个容错 如果 cap 是一个小于等于 0 的数, 经过上面的处理, n 将会变成一个负数
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3.3 给定一个 Map 的构造函数

public HashMap(Map<? extends K, ? extends V> m) {
    // 负载因子 = 0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

/**
 * 放入数据
 * @param m 数据
 * @param evict 这个参数主用是在插入节点后, 做调整用的, 在 HashMap 中不具备任何作用, 构造参数中直接使用 false, 其他的情况使用 true 即可, 基于 HashMap 实现的 LinkedHashMap 才需要使用到这个参数
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {

    // 获取需要传入的 Map 的数据个数
    int s = m.size();
    // 大于 0 的话, 才出来
    if (s > 0) {
        // 当前存放数据的 table 为空, 表示为初始化
        if (table == null) {
            // 计算出来需要的容量 =  长度 / 赋值因子 + 1
            float ft = ((float)s / loadFactor) + 1.0F;
            // 限制最大的容量 为  2^ 30
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);

            // 计算出来的当前需要的容量 > 实际的容量, 进行新容量的计算
            if (t > threshold)
                // 计算存储数据的数组的容量
                threshold = tableSizeFor(t);

        } else if (s > threshold)
            // 当前存入的数据量直接大于当前阈值, 进行重新扩容
            // 扩容方法在下面的添加数据讲解
            resize();

        // 遍历 Map, 将里面的数据逐个迁移到当前的 Map 中
        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);
        }
    }
}

4 HashMap 的操作方法

4.1 添加数据

public class HashMap<K, V> {

    public V put(K key, V value) {
        // 1. 计算 key 的 hash 值
        // 2. 调用 putVal 进行数据的新增
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * key 计算出 hash 值
     */
    static final int hash(Object key) {
        int h;
        // 对象的 hashCode ^ 对象的 hashCode >>> 16
        // 将 hashCode 的高 16 位与 hashCode 进行异或运算, 主要是为了在 table 的 length 较小的时候, 让高位也参与运算, 减少 hash 冲突, 并且不会有太大的开销

        // 这里做了一个容错, 如果 key 为 0, 那么计算出来的 hash 值为 0, 也就是 HashMap 直接 key 为 0 的情况, 但是只能存一个 key 为 null 的数据, 
        // 后面的 key 为 null, 计算出来的 hash 都是 0, 会把旧值覆盖(默认情况)
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    /**
     * 存值
     * @ hash 存的 key 的hash值
     * @ key  对象的 key
     * @ value 存的值
     * @ onlyIfAbsent 存入的数据已在 HashMap 中存在, 是否有新的 value 替代旧的 value, false 进行修改, true 不修改
     * @ evict 这个值用于 LinkedHashMap, 在HashMap 中没有作用
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

        Node<K,V>[] tab; Node<K,V> p; int n, i;

        // 数组为空 或者 数组的长度为 0, 进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        
        // HashMap 对象的位置是通过 (数组的长度 - 1) & hash, 也就是 hash % (2 ^ n - 1) 的形式
        // 存储的位置为空, 直接将 hash, key, value 封装为新的 Node 节点, 放到指定的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);    
        else {

            // 存储的位置已经有数据了
            Node<K,V> e; K k;
            // 需要放入的位置的第一个节点 的 hash 和 key 一样, 取到这个节点
			if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
				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);
						// 当前的容量已经达到了 8个, 转为红黑树, binCount 从 0 开始遍历的
						if (binCount >= TREEIFY_THRESHOLD - 1)
                            // 这个方法后面
                            treeifyBin(tab, hash);
                        break;    
                    }

                    // 链表中有一个节点的 hash 和 key 和要插入的一样, 取到这个节点, 停止遍历链表
					if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 
                        break;
                    // p = p 的下一个节点, 判断下一个节点
                    p = e                        
                }
            }

            // e 不为空, 表示在数组当前的位置的链表或红黑树中存在一个 key 值和 hash 值和当前要存入的数据一样。
            if (e != null) {
                // 获取旧值
                V oldValue = e.value;
				//  旧值为 null 或者入参的 onlyIfAbsent 为 false, 新值替换旧值
				if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 在 HashMap 中这个是空方法                    
                afterNodeAccess(e);
                return oldValue;
            }

            // 修改次数 + 1, 用于支持 fast-fail 机制
            ++modCount;
            // 当前存储的数据个数达到了阈值, 进行扩容
            if (++size > threshold)
            	resize();
        } 

    	// HashMap 的这个方法没有实现
     	afterNodeInsertion(evict);
       	return null;
    }


    /**
     * 重新扩容
     */
    final Node<K,V>[] resize() {

        // 获取当前数组的引用
        Node<K,V>[] oldTab = table;
        // 当前的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;

        // 当前的阈值
        int oldThr = threshold;
        // 新的容量, 新的阈值
        int newCap, newThr = 0;

        // 当前的容量不为0, 也就是有数据存在了
        if (oldCap > 0) {
            // 当前的容量已经达到最大了, 直接不进行操作
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 直接将阈值设为 int 的最大值
            	// 实际的数组长度依旧为 2^30, 一般情况阈值 threshold = 实际数组的长度 * 负载因子, 负载因子一般都是小于 1 大于 0 的数, 所以 threshold < 数组的实际长度
                // 决定数组是否扩容的则是由 threshold 是否小于当前的数据个数 size, 所以数组的容量还未完全用完了, 就扩容了,
                // 现在的情况是, 旧的数组的容量已经达到了设定的最大容量了, 无法继续扩了, 所以将 threshold 直接设置为 int 的最大值 > 数组的长度, 这样可以继续利用原本因负载因子而无法使用到的空间
                threshold = Integer.MAX_VALUE;
                return oldTab;
            
            // 新容量的值 = 旧数组的容量 * 2
            // 新的容量 < 最大容量 && 旧的容量 >= 初始默认的容量(16),  这种情况下设置新的负载 = 旧的 * 2
            // 那么就存在旧的容量为 2, 4, 8 三种情况, 没有走到下面的 newThr = old << 1, 那么新的阈值为 0 

            // oldCap >= DEFAULT_INITIAL_CAPACITY, 新的阈值什么不是无条件的等于旧的阈值的 2 倍
            // 而是只有在 oldCap >= DEFAULT_INITIAL_CAPACITY, 新的阈值才会是旧阈值的 2 倍
            // 而如果 oldCap 为 2, 4, 8, 则新的阈值为 0, 需要到下面的新的阈值 = 0 的判断进行处理, 具体的分析看下面的备注 1
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;

        } else if (oldThr > 0)
            // 当旧的阈值大于 0, 设置新的容量 = 阈值
            // 能够走到这一步的情况有: 
            // 1. 我们初始时, 只指定了容量, 然后第一次往里面加数据
            // 2. 初始时, 只传递了一个 Map, 然后第一次把 Map 里面的数据放到当前 HashMap 时

            // 新的容量 = 当前的阈值 = threshold, 因为在声明 HashMap 时, 会临时将容量存储到 threshold, 直接赋值过来就是需要的容量了
            newCap = oldThr;
        else { 

            // 当我们创建时, 没有指定容量时, 在第一次放数据, 会走这一步
            // 这里将需要的容量的设置为默认值 16, 新的阈值为 16 * 0.75
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 新的阈值为 0 
        if (newThr == 0) {
            // 一般情况下阈值 = 新容量 * 0.75, 如果数组的容量达到了最大容量, 则直接设置为 int 的最大值
            // 这里新的阈值为 0, 能够走到这一步的情况有:
            // 1. 我们初始时, 只指定了容量, 然后第一次往里面加数据
            // 2. 初始时, 只传递了一个 Map, 然后第一次把 Map 里面的数据放到当前 HashMap 时
            // 3. 扩容时, 旧的容量为 2/4/8 中的一个, 那么 newThr 也是 0, 需要结合上面的扩容的 oldCap >= DEFAULT_INITIAL_CAPACITY 进行分析

            // 计算理论的阈值 = 新的容量 * 负载因子
            float ft = (float)newCap * loadFactor;
            // 新的容量 < 最大值 && 理论的阈值 < 最大值 ? 新的阈值 = 计算出来的阈值 : 新的阈值 = int 的最大值
            // 这里个人感觉有点问题, 具体看下面的 备注 2
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
        }

        // 设置新的阈值
        threshold = newThr;


        // 声明新的数组, 用于存储数据
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // HashMap 中的数组引用修改为新声明的数组
        table = newTab;

        // 数组 重新赋值
        if (oldTab != null) {

            // 遍历旧数组的每一项
            for (int j = 0; j < oldCap; ++j) {

                Node<K,V> e;

                // 获取到当前数组的第 j 个元素
                if ((e = oldTab[j]) != null) {
                    第 j 个元素不为 null, 表示有数据, 需要进行迁移

                    // 旧数组的 j 位置的值设置为空, 方便垃圾回收
                    oldTab[j] = null;

                    if (e.next == null)
                        // 如果 e.next 为空, 则代表旧数组的该位置只有 1 个节点, 那么把这个节点直接放到新数组里面, 就行了
                        // 通过 (节点 的 hash 值 & 新容量 -1 ) 取到新的位置
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        // 原本的这个节点为树节点, 调用 TreeNode 的 split 进行处理
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {
                        
                        // 到了这一步, 说明当前位置存储的是一个 链表, 同时链表的数据存在 2 个以上

                        // 声明了2 个链表, lo 链表, hi 链表, 这 4 个指针, 分别指向了 2 个链表的头和尾 
                        // 其中的 lo 可以理解为 low, 地位链表, hi 为 hight, 高维链表, 
                        // 新的数组的容量是是原来的2倍, 那么 原来的一倍 可以理解为 low, 扩充出来的为 hight
                        // 原本在同一个链表上的节点, 转移到扩容后的数组式, 只可能会被分配到 2 个位置,  和原来一样的位置 或者原来的位置 + oldCap 的位置, 看下面的备注 3 
                        // lo 链表存放的是节点位置不需要修改的节点, hi 就是存储位置变为 原来位置 + oldCap 的节点

                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;

                        Node<K,V> next;
                        do {
                            // 遍历 j 位置上的链表
                            // 临时保存下一个节点
                            next = e.next;

                            // 当前节点的 hash 值 与上旧的容量值等于 0, 那么可以确定这个节点在新数组的位置和原来的一样
                            // 先将其放到低维的链表, 后面在把低维的链表放到新数组的对应位置
                            if ((e.hash & oldCap) == 0) {
                                
                                if (loTail == null)
                                    // 如果loTail为空, 代表该节点为第一个节点
                                    loHead = e;
                                else
                                    // 否则将节点添加在链表的尾部
                                    loTail.next = e;    
                                // 重新设置尾结点
                                loTail = e;

                            } else { 
                                // 不等于 0, 表示这个节点需要存放到新的位置, 先放到高维链表
                                if (hiTail == null)
                                	// 作用同上面
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);


                        if (loTail != null) {
                        	// 设置数组的 j 位置为 lo 链表
                            loTail.next = null;
                            newTab[j] = loHead;
                        }

                        if (hiTail != null) {
                        	// 设置数组的 j + oldCap 为 hi链表
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }    
                }
            }
        }
        // 返回新数组
        return newTab;
    }

    /**
     * 将指定的位置转为树
     * 在 putVal 时, 如果发现链表的长度大于 8 了, 就会调用这个方法, 将链表变为链表
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {

        int n, index; Node<K,V> e;
        // 注意这里: 数组为空, 或者长度还不够 64, 进行扩容, 不进行转换操作
        // 所以链表转为红黑树的还有一个大前提: 当前 HashMap 中存储数据的数组的长度要大于 64, 而不是链表的长度大于 8 就树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 需要处理的位置的链表不为 null
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
            	// 因为 TreeNode 继承了 Node, 所以 TreeNode 也可以当做链表使用
            	// 将 Node 转为 TreeNode
            	// 然后将下面拼接成一个以 TreeNode 为类型的链表
                TreeNode<K,V> p = replacementTreeNode(e, null);
                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);
        }
    }
}
4.1.1 备注 1

在 resize 方法中, 旧数组已有数据了, 但是在进行扩容时, 容量默认是 2 倍扩展, 但是阈值只有在 oldCap >= DEFAULT_INITIAL_CAPACITY 时才会进行 2 倍扩展, 否则是为新的容量 * 负载因子
原因: 为了在容量少的情况下, 尽可能的利用数组的空间, 不造成浪费。

假设我们在初始时, 指定了容量为 2, 那么在初始后容量值为 2, 阈值为 2 * 0.75 = 1。

第一次扩容:

旧的容量 新的容量 旧的阈值 经过运算新的阈值
旧阈值 << 1 2 4 1 2
新容量 × 0.75 2 4 1 3

第一次扩容:

旧的容量 新的容量 旧的阈值 经过运算新的阈值
旧阈值 << 1 4 8 2 4
新容量 × 0.75 4 8 3 6

第三次扩容

旧的容量 新的容量 旧的阈值 经过运算新的阈值
旧阈值 << 1 8 16 4 8
新容量 × 0.75 8 16 6 12

第四次扩容

旧的容量 新的容量 旧的阈值 经过运算新的阈值
旧阈值 << 1 16 32 8 16
新容量 × 0.75 16 32 12 24

通过上面可以发现: 在旧容量 < 16 之间变化时, 通过新容量 × 负载因子, 阈值会大一些, 可以更充分的利用数组的空间, 在第四次扩容时, 新容量为 32 时, 旧的阈值为 12, 这时 12 << 1 等于 32 * 0.75, 所以 2 者后续的计算是一样的, 通过位运算比较快。

可以简单理解为: 阈值一直都是等于当前的容量 * 负载因子,
在旧容量为 2, 4, 8 按照上面的方式计算。
到了就容量 >= 16 时, 可以通过位运算达到同样的计算效果, 使用位运算更快

4.1.2 备注 2

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) {

        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        // 步骤 1    
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;

    } else if (oldThr > 0)
        // 省略
    else { 
        // 省略
    }

    if (newThr == 0) {
        // 步骤 2
        float ft = (float)newCap * loadFactor;
        // 步骤 3
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    // 设置新的阈值
    threshold = newThr;
}

上面的阈值的计算一般情况下是没问题的, 但是在容量从 2^29 到最大值 2^30 扩容时, 按照上面的步骤

  1. 走到步骤 1 时, 容量变为原来的 2 倍, newCap = MAXIMUM_CAPACITY, 这时 newCap < MAXIMUM_CAPACITY, 不满足条件, 所以 newThr 还是 0
  2. 到了步骤 2 时, 计算出来的 ft = newCap * loadFactor, loadFactory 一般都是大于 0, 小于 1 的数, 所以 ft < newCap = MAXIMUM_CAPACITY, 既 ft < MAXIMUM_CAPACITY
  3. 到了步骤 3 时, new = MAXIMUM_CAPACITY < MAXIMUM_CAPACITY 为 false, false && true = false
  4. 经过步骤 3 后, newThr 直接变为 int.max_value, 正常应该是 newCap * loadFactor, 下次扩容时, 到达上面的旧容量 >= MAXIMUM_CAPACITY, 再把阈值修改为 int.max_value

上面的分析都是基于负载因子大于 0, 小于 1 的情况的分析, 如果用户将负载因子设置为大于 1 的情况, 可能导致步骤 2 计算出来的 ft 为 负数。
所以可以将上面的步骤 1 修改为 <= MAXIMUM_CAPACITY 或者步骤 3 修改为 <= MAXIMUM_CAPACITY

4.1.3 备注 3

(e.hash & oldCap) == 0 可以判断节点在新数组的位置和旧数组的是否一样, 很巧妙的利用了数组的长度为 2 的 n 次方这个特性。 也就是 HashMap 中数组的容量是 2 的 n 次方的第二个作用。

能达到这样的效果的必须知道:

  1. oldCap 是 2 ^ n, newCap 是 在 oldCap 的基础 * 2, 也就是 newCap 是 2 ^ (n + 1)
  2. 2 ^ n 在二进制的表示为 1 + n 个 0 , 2 ^ (n + 1) 二进制表示 1 + ( n + 1 ) 个 0
  3. 2 ^ n - 1 在二进制的表示为 n 个 1, 那么 2 ^ (n + 1) 就是 n + 1 个 1
  4. (2 ^ n - 1) & hash, 我们只需要取 hash 的从右到左的 1 到 n 位就行了, 因为 2 ^ n - 1 前面都是 0, & 上都是 0

我们假设有个节点 A, 其 现在在数组中的 index 位置, 现在 oldCap 是 16, 既 2^4, 16 - 1 = 15, 二进制表示为 00000000 00000000 00000000 00001111

这时候 index = hash & (16 - 1), index 的值自然就是只和 hash 值的低 4 位有关, 我们假设它为 abcd

oldCap 扩大了一倍, 当前节点的的位置 index 的计算公式 = (32-1) & hash, 和 hash 值的低 5 位有关, hash 的低 5 位的值无外乎下面两种情况: 0abcd 或者 1abcd

0abcd = index, 而 1abcd = 0abcd + 10000 = 0abcd + oldCap = index + oldCap, 从这里可以知道容量扩大了一倍, 那么新的 index 是有规律的, 要么不变, 要么就是 index + oldCap

新旧 index 是否一致就体现在 hash 值的第 5 位, 那么第 5 位怎么知道呢? 32 的 2^n 的二进制形式 1 + 5 个 0, 那么 hash & oldCap 就能知道 hash 的第 5 位是 0 或者 1 了, 既 hash & oldCap = 0, hash 的第 5 位为 0,
hash & oldCap 不等于 0, hash 的第 5 位为 1。

最终可以通过 hash & oldCap 得到

  1. hash & oldCap = 0 => 当前节点在数组的位置不用变
  2. hash & oldCap != 0 => 当前节点在新数组的 index + oldCap 的位置

4.2 其他添加数据的方式

  1. 直接添加一个 Map 的 putAll(Map map);
  2. 添加一个元素, 如果对应的位置已经有数据了, 则不添加 putIfAbsent(K key, V value)

4.3 获取数据

public class HashMap<K, V> {

    /** 
     * 通过 key 获取 value, 如果节点为 null, 返回 nulL
     */
    public V get(Object key) {
        Node<K,V> e;

        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * 通过 key 的 hash 值和 key 值获取 value 值
     */
    final Node<K,V> getNode(int hash, Object key) {

        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

        // 数组不为空, 长度大于 0, 链表的第一个节点 不为 null
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {

        	// 第一个节点的 hash 值和传入的 hash 值一样, key 值也一样,  第一个节点符合条件了, 直接返回
            if (first.hash == hash &&  ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

            // 链表有下一个节点    
            if ((e = first.next) != null) {
                // 树节点, 转换为树节点进行处理
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    // 遍历 链表的其他节点,
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
}

4.4 删除数据

public class HashMap<K, V> {

    /**
	 * 直接通过 key删除
	 */
	public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    }

    /**
     * 删除节点
     * matchValue : 为 true, 找到了节点, 还会比较他们的值, 值相同才会删除
     * movable : 针对红黑树起作用, 为 false, 节点删除了, 不改变其他节点的位置
     */
    final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {

        // 数组不为 null, 长度大于 0, 同时定位到的位置不为 null
        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;
            // 链表的下一个节点不为null
            else if ((e = p.next) != null) {    

                // 节点为 树节点
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                     // 遍历链表
                    do {
                    	// 寻找找到符合条件
                        if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }    
            }

            // 找到了需要删除的节点, 如果设置了需要检查值, 后面会对其值的内存地址和 equals 进行比较 
            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 在上面的链表的遍历中, 变成需要删除的节点的前一个节点了
                    // p 的下一个节点指向需要删除节点的下一个节点
                    p.next = node.next;

                ++modCount;
                --size;
                // 空方法
                afterNodeRemoval(node);
                return node;    
            }
        }
    }
}

上面只是通过 key 进行删除数据的, 同样还有一个通过 key 和 value 值都相同的情况进行删除的方法。

4.5 修改数据

public class HashMap<K, V> {

    public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;

        // 通过 getNode 获取到对应的节点, 节点的值等于入参的 oldValue
        if ((e = getNode(hash(key), key)) != null && ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            // 替换节点的 value 值等于入参的新 value
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }
}

5 HashMap 的补充

  1. HashMap 同样是线程不安全的, 如果需要线程安全, 同样可以通过 Collections.synchronizedMap() 获取到一个线程安全的实现类。
  2. HashMap 支持 fail-fast 机制
  3. HashMap 的序列化也是自定义的
  4. HashMap 内部提供了许多迭代器: KeyIterator, ValueIterator, EntryIterator, 方便各种遍历需求
  5. HashMap 的线程不安全, 猜测时, 同时有 2 个线程判断到需要扩容了, 然后同时进行扩容, 期间就可能造成闭合的链路。

6 红黑树相关的内容

  1. 清晰理解红黑树的演变—红黑的含义 红黑树的前身
  2. 教你初步了解红黑树 红黑树的节点插入
  3. 红黑树之删除节点 红黑树的节点删除
  4. 红黑树原理以及插入、删除算法 附图例说明 里面的增删图片不错

7 参考

Java集合: HashMap详解 (JDK 1.8)
深入理解HashMap(四): 关键源码逐行分析之resize扩容

你可能感兴趣的:(Java,java,哈希算法,开发语言)