[Java集合]Map源码分析:HashMap(上)

1 数据结构

在这里插入图片描述
  HashMap的数据结构是数组+链表+红黑树(红黑树since JDK1.8)。我们常把数组中的每一个节点称为一个。当向桶中添加一个键值对时,首先计算键值对中key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这种现象称为碰撞,这时按照尾插法(jdk1.7及以前为头插法)的方式添加key-value到同一hash值的元素的后面,链表就这样形成了。当链表长度超过8(TREEIFY_THRESHOLD)时,链表就转换为红黑树。

2 顶部注释

HashMap是Map接口基于哈希表的实现。这种实现提供了所有可选的Map操作,并允许key和value为null(除了HashMap是unsynchronized的和允许使用null外,HashMap和HashTable大致相同。)。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
  此实现假设哈希函数在桶内适当地分布元素,为基本实现(get 和 put)提供了稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。如果遍历操作很重要,就不要把初始化容量initial capacity设置得太高(或将加载因子load factor设置得太低),否则会严重降低遍历的效率。
  HashMap有两个影响性能的重要参数:初始化容量initial capacity、加载因子load factor。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。initial capacityload factor就是当前允许的最大元素数目,超过initial capacityload factor之后,HashMap就会进行rehashed操作来进行扩容,扩容后的的容量为之前的两倍。
  通常,默认加载因子 (0.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生rehash 操作。
  如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
  注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(…));
  由所有此类的“collection 视图方法”所返回的迭代器都是fail-fast 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的remove方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
  注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测bug。

总结:

  • 底层:HashMap是Map接口基于哈希表的实现。
  • 是否允许null:HashMap允许key和value为null。
  • 是否有序:HashMap不保证映射的顺序,特别是它不保证该顺序恒久不变。
  • 何时rehash:超出当前允许的最大容量。initial capacityload factor就是当前允许的最大元素数目,超过initial capacityload factor之后,HashMap就会进行rehashed操作来进行扩容,扩容后的的容量为之前的两倍。
  • 初始化容量对性能的影响:不应设置地太小,设置地小虽然可以节省空间,但会频繁地进行rehash操作。rehash会影响性能。总结:小了会增大时间开销(频繁rehash);大了会增大空间开销(占用了更多空间)和时间开销(影响遍历)。
  • 加载因子对性能的影响:加载因子过高虽然减少了空间开销,但同时也增加了查询成本。0.75是个折中的选择。总结:小了会增大时间开销(频繁rehash);大了会也增大时间开销(影响遍历)。
    是否同步:HashMap不是同步的。
  • 迭代器:迭代器是fast-fail的。

3 源码解析

3.1 定义

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {}
  • HashMap<K,V>:HashMap是以key-value形式存储数据的。
  • extends AbstractMap<K,V>:继承了AbstractMap,大大减少了实现Map接口时需要的工作量。
  • implements Map<K,V>:实现了Map,提供了所有可选的Map操作。
  • implements Cloneable:表明其可以调用clone()方法来返回实例的field-for-field拷贝。
  • implements Serializable:表明该类是可以序列化的。

注释:

Implementation notes.
This map usually acts as a binned (bucketed) hash table, but when bins get too large, they are transformed into bins of TreeNodes, each structured similarly to those in java.util.TreeMap. Most methods try to use normal bins, but relay to TreeNode methods when applicable (simply by checking instanceof a node). Bins of TreeNodes may be traversed and used like any others, but additionally support faster lookup when overpopulated. However, since the vast majority of bins in normal use are not overpopulated, checking for existence of tree bins may be delayed in the course of table methods.
HashMap通常是一个binned(bucketed)哈希表,但是当bins变得太大时,会变成TreeNodes,每个结构与 java.util.TreeMap中的结构类似。大多数方法都尝试使用普通的bins,但是有时会用TreeNode方法(只需检查节点的实例)。 TreeNodes的Bins可以像其他任何一样遍历和使用,但是当数量过多时还支持更快的查找。但是,由于正常使用的绝大多数bins都不会体量过大,因此在table方法的过程中可能会延迟检查tree bins的存在。
Tree bins (i.e., bins whose elements are all TreeNodes) are ordered primarily by hashCode, but in the case of ties, if two elements are of the same “class C implements Comparable < C >”, type then their compareTo method is used for ordering. (We conservatively check generic types via reflection to validate this – see method comparableClassFor). The added complexity of tree bins is worthwhile in providing worst-case O(log n) operations when keys either have distinct hashes or are orderable, Thus, performance degrades gracefully under accidental or malicious usages in which hashCode() methods return values that are poorly distributed, as well as those in which many keys share a hashCode, so long as they are also Comparable. (If neither of these apply, we may waste about a factor of two in time and space compared to taking no precautions. But the only known cases stem from poor user programming practices that are already so slow that this makes little difference.)
Tree bins(即其元素都是TreeNodes的bins)主要通过hashCode排序,但如果两个元素具有相同的“class C implements Comparable < C >”,则使用其compareTo方法用于排序。 (我们保守地通过反射来检查泛型类型以验证这一点 - 请参阅方法comparableClassFor)。无论在不同hash值或可排序的情况下都证明算法最坏情况复杂度是 O(log n),所以tree bins 带来的复杂度是值得的。因此,即时在hash出来的值不够充分的分散以及许多键共享一个hashCode的情况下,性能变差的过程也会比较平滑. (如果这两种方法都不适用,同时不采取任何预防措施,我们可能会在时间和空间上浪费大约两倍的时间。 但是,唯一已知的案例源于糟糕的用户编程实践,这些实践已经非常缓慢,这几乎没有什么区别。)
Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution ( http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) pow(0.5, k) / factorial(k)). The first values are:
由于TreeNodes的大小约为常规节点的两倍,因此只有当bin包含足够的节点以保证使用时,我们才使用它们(请参阅TREEIFY_THRESHOLD)。 当它们变得太小(由于移除或调整大小)时,它们会转换回普通bins。 在具有良好分布的用户hashCodes的用法中,很少使用tree bins。 理想情况下,在随机hashCodes下,bin中节点的频率遵循Poisson分布(http://en.wikipedia.org/wiki/Poisson_distribution),参数平均值约为0.5,默认调整阈值为0.75,尽管由于调整粒度而导致的大差异。 忽略方差,列表大小k的预期出现是(exp(-0.5)pow(0.5,k)/ factorial(k))。第一个值是:
0:0.60653066
1:0.30326533
2:0.07581633
3:0.01263606
4:0.00157952
5:0.00015795
6:0.00001316
7:0.00000094
8:0.00000006
more: less than 1 in ten million
公式进行解析:
exp : 指数函数
pow : 乘方运算
factorial : 阶乘
(exp(-0.5) * pow(0.5, k) / factorial(k)) 这个公式是可以对应到泊松分布的公式的.
这个0.5的意思是表示在这里假定元素数量占桶数量的百分50,而threshold是0.75,元素在某个桶里的概率是0.5.
所以我们以这个概率为基础数据算出,桶里有1-8个元素的概率,如数据.当有8个元素在一个桶里时的概率非常低,
在这里也解释了,如果出现需要将链表转成树的情况出现,已经表示不合理的场景出现了.
The root of a tree bin is normally its first node. However, sometimes (currently only upon Iterator.remove), the root might be elsewhere, but can be recovered following parent links (method TreeNode.root()).
树bin的根通常是它的第一个节点。 但是,有时(目前仅在Iterator.remove上),根可能在其他地方,但可以在父链接之后恢复(方法TreeNode.root())。
All applicable internal methods accept a hash code as an argument (as normally supplied from a public method), allowing them to call each other without recomputing user hashCodes. Most internal methods also accept a “tab” argument, that is normally the current table, but may be a new or old one when resizing or converting.
所有适用的内部方法都接受一个hash code作为参数(通常从公共方法提供),允许它们相互调用而无需重新计算用户hashCodes。 大多数内部方法也接受“tab”参数,通常是当前表,但在调整大小或转换时可能是新的或旧的。
When bin lists are treeified, split, or untreeified, we keep them in the same relative access/traversal order (i.e., field Node.next) to better preserve locality, and to slightly simplify handling of splits and traversals that invoke iterator.remove. When using comparators on insertion, to keep a total ordering (or as close as is required here) across rebalancings, we compare classes and identityHashCodes as tie-breakers.
当bin lists被 treeified, split或 untreeified时,我们将它们保持在相同的相对访问/遍历顺序(i.e., field Node.next)中以更好地保留局部性,并略微简化对调用iterator.remove的拆分和遍历的处理。 当在插入时使用比较器时,为了保持整个重新排序的总排序(或者在这里需要尽可能接近),我们将类和identityHashCodes作为绑定器进行比较。
The use and transitions among plain vs tree modes is complicated by the existence of subclass LinkedHashMap. See below for hook methods defined to be invoked upon insertion, removal and access that allow LinkedHashMap internals to otherwise remain independent of these mechanics. (This also requires that a map instance be passed to some utility methods that may create new nodes.)
由于子类LinkedHashMap的存在,普通(plain)与树模型(tree modes)之间的使用和转换变得复杂起来。请参阅下面的hook方法,这些方法在插入、删除和访问时被调用,允许LinkedHashMap内部结构保持独立于这些机制。(这还要求将Map实例传递给一些可能创建新节点的实用方法。)
The concurrent-programming-like SSA-based coding style helps avoid aliasing errors amid all of the twisty pointer operations.

3.2 静态全局变量

/**
 * 默认初始化容量,值为16
 * 必须是2的n次幂.
 */ 
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 
 /**
 * 最大容量, 容量不能超出这个值。如果一个更大的初始化容量在构造函数中被指定,将被MAXIMUM_CAPACITY替换.
 * 必须是2的倍数。最大容量为1<<30,即2的30次方。
 */ 
 static final int MAXIMUM_CAPACITY = 1 << 30; 
 /**
 * 默认的加载因子。
 */ 
 static final float DEFAULT_LOAD_FACTOR = 0.75f; 
 /**
 * 将链表转化为红黑树的临界值。
 * 当添加一个元素被添加到有至少TREEIFY_THRESHOLD个节点的桶中,桶中链表将被转化为树形结构。
 * 临界值最小为8
 */ 
 static final int TREEIFY_THRESHOLD = 8; 
 /**
 * 恢复成链式结构的桶大小临界值
 * 小于TREEIFY_THRESHOLD,临界值最大为6
 */ 
 static final int UNTREEIFY_THRESHOLD = 6;
 /**
 * 桶可能被转化为树形结构的最小容量。当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采取扩容来尝试减少冲突。
 * 应该至少4*TREEIFY_THRESHOLD来避免扩容和树形结构化之间的冲突。
 */ 
 static final int MIN_TREEIFY_CAPACITY = 64;

3.3 静态内部类 Node

/**
 * HashMap的节点类型。既是HashMap底层数组的组成元素,又是每个单向链表的组成元素
 */
static class Node<K,V> implements Map.Entry<K,V> {
    //key的哈希值
    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;
    }
}

3.4 静态方法

hash( Object key)

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  1. 看出key是可以空的,此时hash为0;
  2. 计算位置分为两步,第一步,取key的hashCode,第二步,key的hashCode高16位异或低16位。
    做异或运算的原因:
    表默认的初始容量是16,要放到散列表中,就是0-15的位置上(0000 0000 0000 0000 0000 0000 0000 1111),这样的值如果跟hashCode()直接做与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,这样很容易造成碰撞,所以把高低位都参与到计算中,从而解决了这个问题,而且也不会有太大的开销。

comparableClassFor( Object x)

	/**
 	* 如果参数x实现了Comparable接口,返回参数x的类名,否则返回null
	*/
    static Class<?> comparableClassFor(Object x) {
        if (x instanceof Comparable) { // 判断是否实现了Comparable接口
            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
            if ((c = x.getClass()) == String.class) // // 如果是String类型,直接返回String.class
                return c; 
            if ((ts = c.getGenericInterfaces()) != null) { // 判断是否有直接实现的接口
                for (int i = 0; i < ts.length; ++i) { // 遍历直接实现的接口
                    if (((t = ts[i]) instanceof ParameterizedType) && // 该接口实现了泛型
                        ((p = (ParameterizedType)t).getRawType() == // 获取接口不带参数部分的类型对象
                         Comparable.class) &&  //  该类型是Comparable
                        (as = p.getActualTypeArguments()) != null && // 获取泛型参数数组
                        as.length == 1 && as[0] == c) //  // 只有一个泛型参数,且该实现类型是该类型本身
                        return c;
                }
            }
        }
        return null;
    }

参考博客:https://blog.csdn.net/qpzkobe/article/details/79533237

compareComparables( Class<?> kc, Object k, Object x)

/**
 * 如果x的类型为kc,则返回k.compareTo(x),否则返回0.
 */
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}

tableSizeFor( int cap)

/**
 * 返回大于等于cap的最小的二次幂数值。
 */
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;
}

3.5 Fields

/**
 * 存储键值对的数组,一般是2的幂
 */
transient Node<K,V>[] table;

/**
 * 键值对缓存,它们的映射关系集合保存在entrySet中。即使Key在外部修改导致hashCode变化,缓存中还可以找到映射关系
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 键值对的实际个数
 */
transient int size;

/**
 * 记录HashMap被修改结构的次数。
 * 修改包括改变键值对的个数或者修改内部结构,比如rehash
 * 这个域被用作HashMap的迭代器的fail-fast机制中(参考ConcurrentModificationException)
 */
transient int modCount;

/**
 * 扩容的临界值,通过capacity * load factor可以计算出来。超过这个值HashMap将进行扩容
 * @serial
 */

int threshold;

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

3.6 构造方法

HashMap( int initialCapacity, float loadFactor)

/**
 * 使用指定的初始化容量initial capacity 和加载因子load factor构造一个空HashMap
 *
 * @param  initialCapacity 初始化容量
 * @param  loadFactor      加载因子
 * @throws IllegalArgumentException 如果指定的初始化容量为负数或者加载因子为非正数。
 */
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);
}

HashMap( int initialCapacity)

/**
 * 使用指定的初始化容量initial capacity和默认加载因子DEFAULT_LOAD_FACTOR(0.75)构造一个空HashMap
 *
 * @param  initialCapacity 初始化容量
 * @throws IllegalArgumentException 如果指定的初始化容量为负数
 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

HashMap()

/**
 * 使用指定的初始化容量(16)和默认加载因子DEFAULT_LOAD_FACTOR(0.75)构造一个空HashMap
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

HashMap( Map<? extends K, ? extends V>m)

/**
 * 使用指定的初始化容量(16)和默认加载因子DEFAULT_LOAD_FACTOR(0.75)构造一个空HashMap
 */
  public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false); //
    }

3.7 核心方法

get(Object key)

/**
 * 返回指定的key映射的value,如果value为null,则返回null。
 *
 * @see #put(Object, Object)
 */
public V get(Object key) {
    Node<K,V> e;
    //如果通过key获取到的node为null,则返回null,否则返回node的value。getNode方法的实现就在下面。
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
  1. 通过hash(Object key)方法计算key的哈希值hash。
  2. 通过getNode( int hash, Object key)方法获取node。
  3. 如果node为null,返回null,否则返回node.value。

接下来我们看看getNode()是怎么实现的:
getNode( int hash, Object key)

/**
 * 根据key的哈希值和key获取对应的节点
 * 
 * @param hash 指定参数key的哈希值
 * @param key 指定参数key
 * @return 返回node,如果没有则返回null
 */
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) {
        //如果桶中的第一个节点就和指定参数hash和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) {
            //如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //如果当前的桶不采用红黑树,即桶中节点结构为链式结构
            do {
                //遍历链表,直到key匹配
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //如果哈希表为空,或者没有找到节点,返回null
    return null;
}

getNode方法分为以下几个步骤:

  1. 如果哈希表为空,或key对应的桶为空,返回null
  2. 如果桶中的第一个节点就和指定参数hash和key匹配上了,返回这个节点。
  3. 如果桶中的第一个节点没有匹配上,而且有后续节点
    3.1 如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
    3.2 如果当前的桶不采用红黑树,即桶中节点结构为链式结构,遍历链表,直到key匹配
  4. 找到节点返回null,否则返回null。

put( K key, V value)

/**
 * 将指定参数key和指定参数value插入map中,如果key已经存在,那就替换key对应的value
 * 
 * @param key 指定key
 * @param value 指定value
 * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
 */
public V put(K key, V value) {
    //putVal方法的实现就在下面
    return putVal(hash(key), key, value, false, true);
}

put(K key, V value)可以分为三个步骤:

  1. 通过hash(Object key)方法计算key的哈希值。
  2. 通过putVal(hash(key), key, value, false, true)方法实现功能。
  3. 返回putVal方法返回的结果。

下面看看putVal方法是如何实现的。

putVal( int hash, K key, V value, boolean onlyIfAbsent,boolean evict)

/**
 * Map.put和其他相关方法的实现需要的方法
 * 
 * @param hash 指定参数key的哈希值
 * @param key 指定参数key
 * @param value 指定参数value
 * @param onlyIfAbsent 如果为true,即使指定参数key在map中已经存在,也不会替换value
 * @param evict 如果为false,数组table在创建模式中
 * @return 如果value被替换,则返回旧的value,否则返回null。当然,可能key对应的value就是null。
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果哈希表为空,调用resize()创建一个哈希表,并用变量n记录哈希表长度
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果指定参数hash在表中没有对应的桶,即为没有碰撞
    if ((p = tab[i = (n - 1) & hash]) == null)
        //直接将键值对插入到map中即可
        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))))
            //将桶中的第一个节点记录起来
            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);
                    //如果链的长度大于TREEIFY_THRESHOLD这个临界值,则把链变为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    //跳出循环
                    break;
                }
                //如果找到了重复的key,判断链表中结点的key值与插入的元素的key值是否相等,如果相等,跳出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        //如果key映射的节点不为null
        if (e != null) { // existing mapping for key
            //记录节点的vlaue
            V oldValue = e.value;
            //如果onlyIfAbsent为false,或者oldValue为null
            if (!onlyIfAbsent || oldValue == null)
                //替换value
                e.value = value;
            //访问后回调
            afterNodeAccess(e);
            //返回节点的旧值
            return oldValue;
        }
    }
    //结构型修改次数+1
    ++modCount;
    //判断是否需要扩容
    if (++size > threshold)
        resize();
    //插入后回调
    afterNodeInsertion(evict);
    return null;
}

putVal方法可以分为下面的几个步骤:

  1. 如果哈希表为空,调用resize()创建一个哈希表。
  2. 如果指定参数hash在表中没有对应的桶,即为没有碰撞,直接将键值对插入到哈希表中即可。
  3. 如果有碰撞,遍历桶,找到key映射的节点
    3.1 桶中的第一个节点就匹配了,将桶中的第一个节点记录起来。
    3.2 如果桶中的第一个节点没有匹配,且桶中结构为红黑树,则调用红黑树对应的方法插入键值对。
    3.3 如果不是红黑树,那么就肯定是链表。遍历链表,如果找到了key映射的节点,就记录这个节点,退出循环。如果没有找到,在链表尾部插入节点。插入后,如果链的长度大于TREEIFY_THRESHOLD这个临界值,则使用treeifyBin方法把链表转为红黑树。
  4. 如果找到了key映射的节点,且节点不为null
    4.1 记录节点的vlaue。
    4.2 如果参数onlyIfAbsent为false,或者oldValue为null,替换value,否则不替换。
    4.3 返回记录下来的节点的value。
  5. 如果没有找到key映射的节点(2、3步中讲了,这种情况会插入到hashMap中),插入节点后size会加1,这时要检查size是否大于临界值threshold,如果大于会使用resize方法进行扩容。

接下来我们看看resize()方法,在初始化的时候要调用这个方法,当散列表元素大于capacity * load factor(临界值threshold)的时候也是调用resize()。

resize()

resize方法非常巧妙,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。

/**
 * 对table进行初始化或者扩容。
 * 如果table为null,则对table进行初始化
 * 如果对table扩容,因为每次扩容都是翻倍,与原来计算(n-1)&hash的结果相比,节点要么就在原来的位置,要么就被分配到“原位置+旧容量”这个位置。
 */
final Node<K,V>[] resize() {
    //新建oldTab数组保存扩容前的数组table
    Node<K,V>[] oldTab = table;
    //使用变量oldCap扩容前table的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //保存扩容前的临界值
    int oldThr = threshold;
    int newCap, newThr = 0;
    //如果扩容前的容量 > 0
    if (oldCap > 0) {
        //如果当前容量>=MAXIMUM_CAPACITY
        if (oldCap >= MAXIMUM_CAPACITY) {
            //扩容临界值提高到正无穷
            threshold = Integer.MAX_VALUE;
            //无法进行扩容,返回原来的数组
            return oldTab;
        }
        //如果现在容量的两倍小于MAXIMUM_CAPACITY且现在的容量大于DEFAULT_INITIAL_CAPACITY
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)
            //临界值变为原来的2倍
            newThr = oldThr << 1; 
    }//如果旧容量 <= 0,而且旧临界值 > 0
    else if (oldThr > 0) 
        //数组的新容量设置为老数组扩容的临界值
        newCap = oldThr;
    else {//如果旧容量 <= 0,且旧临界值 <= 0,新容量扩充为默认初始化容量,新临界值为DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {//在当上面的条件判断中,只有oldThr > 0成立时,newThr == 0
        //ft为临时临界值,下面会确定这个临界值是否合法,如果合法,那就是真正的临界值
        float ft = (float)newCap * loadFactor;
        //当新容量< MAXIMUM_CAPACITY且ft < (float)MAXIMUM_CAPACITY,新的临界值为ft,否则为Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //将扩容后hashMap的临界值设置为newThr
    threshold = newThr;
    //创建新的table,初始化容量为newCap
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //修改hashMap的table为新建的newTab
    table = newTab;
    //如果旧table不为空,将旧table中的元素复制到新的table中
    if (oldTab != null) {
        //遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果旧桶不为null,使用e记录旧桶
            if ((e = oldTab[j]) != null) {
                //将旧桶置为null
                oldTab[j] = null;
                //如果旧桶中只有一个node
                if (e.next == null)
                    //将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置
                    newTab[e.hash & (newCap - 1)] = e;
                //如果旧桶中的结构为红黑树
                else if (e instanceof TreeNode)
                    //将树中的node分离
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { //如果旧桶中的结构为链表。这段没有仔细研究
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //遍历整个链表中的节点
                    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);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

从代码中可以看到,扩容很耗性能。所以在使用HashMap的时候,先估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。

看完代码后,可以将resize的步骤总结为

  1. 计算扩容后的容量,临界值
  2. 将hashMap的临界值修改为扩容后的临界值
  3. 根据扩容后的容量新建数组,然后将hashMap的table的引用指向新数组。
  4. 将旧数组的元素复制到table中。

remove( Object key)

/**
 * 删除hashMap中key映射的node
 *
 * @param  key 参数key
 * @return 如果没有映射到node,返回null,否则返回对应的value。
 */
public V remove(Object key) {
    Node<K,V> e;
    //根据key来删除node。removeNode方法的具体实现在下面
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

remove方法的实现可以分为三个步骤:

  1. 通过hash(Object key)方法计算key的哈希值。
  2. 通过removeNode方法实现功能。
  3. 返回被删除的node的value。

下面看看removeNode方法的具体实现

removeNode( int hash, Object key, Object value,boolean matchValue, boolean movable)

/**
 * Map.remove和相关方法的实现需要的方法
 * 删除node
 * 
 * @param hash key的哈希值
 * @param key 参数key
 * @param value 如果matchValue为true,则value也作为确定被删除的node的条件之一,否则忽略
 * @param matchValue 如果为true,则value也作为确定被删除的node的条件之一
 * @param movable 如果为false,删除node时不会删除其他node
 * @return 返回被删除的node,如果没有node被删除,则返回null(针对红黑树的删除方法)
 */
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不为空且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;
        //如果桶上第一个node的就是要删除的node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //记录桶上第一个node
            node = p;
        else if ((e = p.next) != null) {//如果桶内不止一个node
            if (p instanceof TreeNode)//如果桶内的结构为红黑树
                //记录key映射到的node
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {//如果桶内的结构为链表
                do {//遍历链表,找到key映射到的node
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        //记录key映射到的node
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //如果得到的node不为null且(matchValue为false||node.value和参数value匹配)
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            //如果桶内的结构为红黑树
            if (node instanceof TreeNode)
                //使用红黑树的删除方法删除node
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)//如果桶的第一个node的就是要删除的node
                //删除node
                tab[index] = node.next;
            else//如果桶内的结构为链表,使用链表删除元素的方式删除node
                p.next = node.next;
            //结构性修改次数+1
            ++modCount;
            //哈希表大小-1
            --size;
            afterNodeRemoval(node);
            //返回被删除的node
            return node;
        }
    }
    //如果数组table为空或key映射到的桶为空,返回null。
    return null;
}

removeNode方法的步骤总结为

  1. 如果数组table为空或key映射到的桶为空,返回null。
  2. 如果key映射到的桶上第一个node的就是要删除的node,记录下来。
  3. 如果桶内不止一个node,且桶内的结构为红黑树,记录key映射到的node。
  4. 桶内的结构不为红黑树,那么桶内的结构就肯定为链表,遍历链表,找到key映射到的node,记录下来。
  5. 如果被记录下来的node不为null,删除node,size-1被删除。
  6. 返回被删除的node。

3.8常用方法

putMapEntries(Map<? extends K, ? extends V> m, boolean evict)

/**
 * Map.putAll and Map constructor的实现需要的方法。
 * 将m的键值对插入本map中
 * 
 * @param m the map
 * @param evict 初始化map时使用false,否则使用true
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    //如果参数map不为空
    if (s > 0) {
        //如果table没有初始化
        if (table == null) { // pre-size
            //前面讲到,initial capacity*load factor就是当前hashMap允许的最大元素数目。那么不难理解,s/loadFactor+1即为应该初始化的容量。
            float ft = ((float)s / loadFactor) + 1.0F;
            //如果ft小于最大容量MAXIMUM_CAPACITY,则容量为ft,否则容量为最大容量MAXIMUM_CAPACITY
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            //如果容量大于临界值
            if (t > threshold)
                //根据容量初始化临界值
                threshold = tableSizeFor(t);
        }
        //table已经初始化,并且map的大小大于临界值
        else if (s > threshold)
            //扩容处理
            resize();
        //将map中所有键值对添加到hashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            //putVal方法的实现在下面
            putVal(hash(key), key, value, false, evict);
        }
    }
}

size()

/**
 * 返回map中键值对映射的个数
 * 
 * @return map中键值对映射的个数
 */
public int size() {
    return size;
}

isEmpty()

/**
 * 如果map中没有键值对映射,返回true
 * 
 * @return <如果map中没有键值对映射,返回true
 */
public boolean isEmpty() {
    return size == 0;
}

containsKey( Object key)

/**
 * 如果map中含有key为指定参数key的键值对,返回true
 * 
 * @param   key   指定参数key
 * @return 如果map中含有key为指定参数key的键值对,返回true
 */
public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

treeifyBin( Node<K,V>[] tab, int hash)

/**
 * 将链表转化为红黑树
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //如果桶数组table为空,或者桶数组table的长度小于MIN_TREEIFY_CAPACITY,不符合转化为红黑树的条件
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //扩容
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {//如果符合转化为红黑树的条件,而且hash对应的桶不为null
        TreeNode<K,V> hd = null, tl = null;
        //遍历链表
        do {
            //替换链表node为树node,建立双向链表
            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);
    }
}

putAll( Map<? extends K, ? extends V> m)

/**
 * 将参数map中的所有键值对映射插入到hashMap中,如果有碰撞,则覆盖value。
 * @param m 参数map
 * @throws NullPointerException 如果map为null
 */
public void putAll(Map<? extends K, ? extends V> m) {
    putMapEntries(m, true);
}

clear()

/**
 * 删除map中所有的键值对
 * The map will be empty after this call returns.
 */
public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

containsValue( Object value)

/**
 * 如果hashMap中的键值对有一对或多对的value为参数value,返回true
 *
 * @param  value whose presence in this map is to be tested
 * @return 如果hashMap中的键值对有一对或多对的value为参数value,返回true
 */
    public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

3.9 红黑树相关

见:[Java集合]Map源码分析:HashMap红黑树解析

其他的内容见:[Java集合]Map源码分析:HashMap(下)

猜你喜欢

转载自blog.csdn.net/qq_41655934/article/details/89339927