Java 集合 3:HashMap 源码分析

HashMap 源码分析

全篇以 Java8 为基础

Java文档

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

Map 接口的哈希表实现。允许包括 null 在内的所有元素作为 keyvalue 。与 Hashtable 类似,除了 HashMap 是线程安全的和允许 null 值。HashMap 不保证插入顺序,同时随着时间推移顺序也有可能改变。

​ HashMap 类的声明如下所示,它实现了 Map 接口,属于 Java Collections Framework 。此外,还是实现了 Cloneable 和 Serializable 接口,说明它可克隆、可序列化。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

​ 它的一个双包胎哥哥就是 Hashtable ,它们两个基本上是一样的,但是除了文档中的提到的两个差异,还有其他几个比较小的不同,但是这些差异都不会影响使用。但是现在 Hashtable 基本不会被用到,用的更多的还是 HashMap。

关键属性

​ HashMap 类的属性如下所示。HashMap 采用数组 (桶) + 链表的形式来存储数据,但是当链表的长度超过一定数量 (TREEIFY_THRESHOLD = 8) 并且数组长度超过一定数量 (MIN_TREEIFY_CAPACITY = 64) ,就会转化为红黑树来存储;在只满足链表的长度超过 TREEIFY_THRESHOLD = 8 时,只会进行 resize 。当然,当桶内的数据数据减少到 (UNTREEIFY_THRESHOLD = 6) 时,会从树转化为链表。

DEFAULT_LOAD_FACTOR 为默认负载因子,表示在进行 rehash 操作之前能存储的最多数据,也就是如果没有 rehash ,则容器最多能存放 capacity * load factor 个数据。rehash 是指在容器内存储的数据数量到达 capacity * load factor 后会进行的操作,扩大数组的长度,重新将数据映射到每个桶上。这样做可以防止进一步的哈希冲突,防止 HashMap 的读写效率下降。

DEFAULT_INITIAL_CAPACITY 为默认容量,即数组长度 (桶的数量) 。注意这里必须为长度必须为 2 的幂次,后续会说明。

/**
* 序列化相关
*/
private static final long serialVersionUID = 362498820763181265L;

/**
 * 默认容量 16
 * 必须为 2 的幂次
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大的容量
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认负载因子
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 链表转化为红黑树的阈值
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 红黑树退化为链表的阈值
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 转化为红黑树的 table 长度阈值
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * 存储数据的数组
 */
transient Node<K,V>[] table;

/**
 * 集合形式的键值对
 */
transient Set<Map.Entry<K,V>> entrySet;

transient int size;

/**
 * 对哈希表修改的计数,用于 fast-fail 机制
 */
transient int modCount;

/**
 * capacity * load factor
 * 如果 table 数组没有初始化,则该项为 capacity 或 0 ( DEFAULT_INITIAL_CAPACITY )	
 */
int threshold;

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

存储结构

​ HashMap 有一个内部类 Node , 正是使用这个类来存储数据的。Node 类其实也是类似于链表中的一个节点,节点存储着 keyvaluehash

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    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;
    }
}

​ 除此之外,还有 EntrySet ,KeySet 等 Set 集合来存放键值对,相当于一个视图,他们都是懒加载的,只有当真正读取里面的内容,才会从上面的 table 数组中获取元素。通常使用 EntrySet 来对 HashMap 进行遍历。

构造函数

​ HashMap 有四个构造函数。

​ 第一个接收两个参数,分别是 HashMap 的初始容量和负载因子。之前提到过 HashMap 的大小必须为 2 的幂次,这里对应输入的 initialCapacity ,就会调用 tableSizeFor(int cap) 函数将容量调整为大于等于 initialCapacity 的最小 2 的幂次。

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

static final int tableSizeFor(int cap) {
    // 减一为了在 cap 本身就是 2 的幂次时,返回的仍然是 cap 原数
    int n = cap - 1;
    n |= n >>> 1; // 执行之后 n 中的 1 变成 2 个连续的 1 
    n |= n >>> 2; // 执行之后 n 中的 1 变成 4 个连续的 1 
    n |= n >>> 4; // 执行之后 n 中的 1 变成 8 个连续的 1 
    n |= n >>> 8; // 执行之后 n 中的 1 变成 16 个连续的 1 
    n |= n >>> 16; // 执行之后 n 中的 1 变成 32 个连续的 1 
    // 最后加 1 即使 2 的幂次
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

​ 第二种构造函数接收一个初始容量,负载因子使用默认的 0.75

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

​ 第三种全是默认,初始容量为 16 ,负载因子为 0.75

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

​ 第四种是使用一个 Map 对象来初始化 HashMap。此时负载因子为默认的 0.75

public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

/**
 * evict 参数在 HashMap 中没用,在 LinkedHashMap 中有用 
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        // 如果数组还没有初始化
        if (table == null) { // pre-size
            // 计算出 HashMap 的容量
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 放入的数量大于阈值则进行 resize
        else if (s > threshold)
            resize();
        // 向 HashMap 中加入数据
        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);
        }
    }
}

​ 前三种构造函数在构造完成之后,会将 HashMap 的容量存放在 threshold 中,0 表示默认容量 16 ,非零就是用户指定的容量。

关键方法

public V put(K key, V value)

​ 该方法将一个键值对放入 HashMap 中。流程如下如所示,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7k3cxMx-1578896694739)(Java%E9%9B%86%E5%90%88%E4%B9%8BHashMap.assets/%E6%9C%AA%E5%91%BD%E5%90%8D%E6%96%87%E4%BB%B6.png)]

​ 在这里如果两个 key 相同,那么必然满足它们的哈希值相等而且 == 或者 equals 返回 true

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果 table 没有初始化或长度为 0 则进行 resize 扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果映射位置没有存放数据则直接存放到这个位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 发生 Hash 冲突
    else {
        Node<K,V> e; K k;
        // 如果该位置的 key 与 要放入的 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 {
            // 遍历链表找到结尾或者遇到相同的 key
            for (int binCount = 0; ; ++binCount) {
                // 遍历到了链表的结尾
                if ((e = p.next) == null) {
                    // 在链表尾部添加节点
                    p.next = newNode(hash, key, value, null);
                    // 节点数量超出了树化阈值就可能转化为红黑树,严格大于 8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 遇到相同的 key
                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;
    // size + 1 ,大于阈值则进行 resize 扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

public V get(Object key)

​ 与 put 方法对应的就是 get 方法。get 方法能根据给定的键返回一个值。如果不存在则返回 null

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // table 不为空 && 长度大于 0 && 映射位置存放着数据
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果第一个节点就是要找的 key 就直接返回
        if (first.hash == hash && // always check first node
            ((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;
}

public V remove(Object key)

​ 这个方法能从 HashMap 中移除一个键值对,并返回被移除的 value

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

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;
    // table 不为空 && 长度大于 0 && 映射位置存放着数据
    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;
        // 如果第一个节点就是要删除的 key 就记录这个节点
        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)
                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);
            }
        }
        // 如果要删除的节点存在
        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;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

public V putIfAbsent(K key, V value)

​ 这个方法只有当键不再 HashMap 中时才会将这个键值对放到容器中取。除此之外,如果容器中的 key 对应的 valuenull 的话,也会将 value 放到容器中去,覆盖掉 null 。因此键值对不存在的充要条件时 key 不在容器中或 key 对应的 valuenull

public V putIfAbsent(K key, V value) {
    return putVal(hash(key), key, value, true, true);
}

public boolean remove(Object key, Object value)

​ 此方法要求被删除的键值对不仅 key 要与容器中的匹配而且 value 也需要匹配。

public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}

哈希函数

​ HashMap 使用对象的 hashCode 来计算它在容器中的映射位置,使用哈希值对数组长度取余数来获取映射位置。但是并不是直接使用 hashCode,而是使用静态方法 hash(Object key) 将对象哈希值的高位和低位异或,这在在映射的过程中高位和地位都可以影响映射位置。

static final int hash(Object key) {
    int h;
    // 将对象 hash 值的高位和低位异或
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

​ 此外,容器在取余数的时候并不是使用 % 运算符来进行的,而是使用 & 运算符进行求余数。p = tab[index = (n - 1) & hash] 这里的 (n - 1) & hash 就时进行取余数操作。但是这样的替换需要数组的长度为 2 的幂次才能有相同的结果,这就是为什么 HashMap 严格要求 table 数组的长度是 2 的幂次。

​ 简单来说,如果哈希表的长度是 2^N ,那么 key 映射的位置就是 hash 值的低 N 位。所以为了减少哈希冲突,将哈希值的高位和地位异或,使它们都能参与哈希映射。

扩容过程

​ 在 HashMap 初始化 table 数组和存放的数据数量超过 threshold 时间都会进行 resize 操作。

resize 操作每次将 table 数组的长度扩大位原来的两倍,这样在计算 key 对应的映射位置的时候从原来的低 N 位变成了 N + 1 位。所以在这个函数里面并没有重新计算映射的位置:如果第 N 位为 0 ,则 resize 前后映射的位置不变;如果第 N 位为 1 ,则 resize 前映射位置位 indexresize 后位置为 index + oldCapacity

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 获取旧容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 获取旧阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 非初始化 table 数组的情况
    if (oldCap > 0) {
        // 如果容量已经到达了最大,则无法进行扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 将 threshold 设置为 Integer.MAX_VALUE 后返回原 table 数组
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 扩容之后不会超出最大容量并且不是初始化 table 数组是的扩容
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 容量翻倍,阈值也翻倍
            newThr = oldThr << 1; // double threshold
    }
    // 初始化 table 的容量存放在 threshold 中
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // threshold 为 0 则是默认容量
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
   	// - 第一次初始化 table 数组时 resize
    //   - 构造函数有 initialcapacity 则使用 initialcapacity 来初始化 newCap , newThr = newCap * DEFAULT_LOAD_FACTOR
    //   - 否则使用默认 DEFAULT_INITIAL_CAPACITY 初始化 newCap , newThr = newCap * loadFactor
    // - 存储的数量超过 threshold 时 resize
    //   - oldCap 已经达到最大,将 threshold 设置为 Integer.Max_VALUE 后返回
    //	 - 将 oldCap 容量翻倍赋值给 newCap ,如果超出最大容量并且对应的阈值也超出最大容量,则将 newThr 赋值为 Integer.MAX_VALUE 否则 newThr = newCap * loadFactor
    
    @SuppressWarnings({"rawtypes","unchecked"})
    // 创建新的 table 数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 非初始化 table 的情况
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 原来的位置只有一个元素,重新计算哈希映射位置后放到新的 table 中
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果时一颗树
                else if (e instanceof TreeNode)
                    // 对树进行拆分,拆分后如果小于树的节点数量小于 6 则需要变成链表
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                // 是链表
                else { // preserve order
                    // 新增位为 0 的链表
                    Node<K,V> loHead = null, loTail = null;
                    // 新增位为 1 的链表
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 下面这个循环根据节点哈希值新增的一位是否为 1 将一条链表分成了两条链表
                    do {
                        next = e.next;
                        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);
                   	// 将链表接入到新的 table 数组中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

总结

​ 从 java7 到 java8, HashMap 最大的变化就是当满足一定条件链表会转化成一颗红黑树,以此保证查找的时间复杂度为 O(lgN) 。此外,HashMap 不是线程安全的,在多线程并发访问的时候,resize 函数可能会导致链表产生环,从而造成死循环,所以在多线程下最后使用 ConcurrentHashMap。同样的,HashMap 的迭代器也是支持 fast-fail 机制的。

发布了14 篇原创文章 · 获赞 0 · 访问量 107

猜你喜欢

转载自blog.csdn.net/aaacccadobe/article/details/103957334