细读HashMap

注意: 文章相对较长,可能会消耗您60分钟的时间。

未经作者允许,禁止转裁。

描述

类开头对HashMap作了详细的描述

HashMap基于MAP接口实现,该实现提供了全部的MAP操作方法,同时也允许null值和null键,(HashMapHashTable非常相似,除了它并不是同步的,而且允许空值), 该类并不能保证数据的存储顺序,尤其是随着时间的推移,并不能保证顺序的一致性。

该实现类提供了对于基本操作getput的一致性能表现,假设你采用了正确的hash函数让所有的元素平均分散到哈希表中。 迭代器迭代集合所需时间与当前HashMap实例容量加上键值对数量成正比关系。 因此,一个关键点就是,如果你想要有一个良好的迭代性能的话,就不要在初始化容量时设置得过大(或者不要把负载因子设置得过小)。

一个HashMap的实例有两个参数会影响到其性能: 初始化容量负载因子, 容量即该HashMap的哈希表大小, 初始化容量是在创建HashMap时所设置的哈希表大小, 负载因子是用于决定哈希表在何时自动扩大其容量的一个阈值点。 当HashMap中的元素数量超过负载因子和当前容量的乘积时,HashMap将调用refresh方法(也就是说,内部元素的结构将被重建),使得HashMap的哈希表大小大约是当前的两倍。

作为一个通用规则, 默认的负载因子(0.75)提供了一个基于时间和空间消耗上的折衷方案。 较高的值会减少空间开销,但会增加查找成本(表现在HashMap的大多数操作中,包括getput)。 在设置初始容量时,应考虑HashMap中的预期元素数量及其负载因子,以尽量减少重散列次数。如果初始容量大于元素最大数量除以负载因子,将不会发生任何重散列。

假设有很多的键值对需要被存储到HashMap实例中,创建一个容量充足的哈希表来存储的话,将会获得更好的性能表现,而不是采用等到必要的时候由自动扩容来增加容量。 需要注意的是,如果很多键都使用同一个hashCode的话,在任何哈希表中都会降低性能。 为了减少影响,当键是可比较的时候,该类可以考虑采用比较的顺序来打破约束。

请注意,该实现类是非同步的。 如果多个线程同时访问这个哈希表,其中至少有一个线程会去修改这个哈希表的结构, 它将必须做外部同步。 (结构改变是指新增或删除一个或多个键值对, 仅仅更改与实例已包含的键关联的值不是结构修改。) 常规的做法是对某个对象进行同步,就很自然的将HashMap包含进来了。

如果没有这个对象存在的话,也可以使用Collections.synchronizedMap方法来对哈希表进行包裹。 最好是在创建的时候就加上,这样可以防止非同步的访问。

Map m = Collections.synchronizedMap(new HashMap(...));

迭代器在所有的集合查看方法中是支持快速失败的:在迭代器创建之后,如果哈希表有任何结构上的改变,除了迭代器自身的remove方法,迭代器将抛出ConcurrentModificationException异常。 因此,在面对并发修改时,迭代器会快速而干净利落地失败,而不是在将来某个不确定的时间冒着任何不确定的行为风险。

请注意,快速失败的行为是不能被保证的,通常情况下,在存在异步并发修改的时候,不能得到完全保证。 快速失败迭代器在尽最大努力的基础上抛出ConcurrentModificationException异常。因此,编写依赖于此异常的程序以确保其正确性是错误的行为: 迭代器的快速失败行为应仅用于检测错误。

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

    private static final long serialVersionUID = 362498820763181265L;

实现说明

HashMap通常是伴演哈希表的存储箱,但是当箱子足够大了之后,它将转换成TreeNodes(树节点), 每一个的结构都和java.util.TreeMap差不多, 绝大部分方法都会去使用常见的存储箱, 但在适当的情况下转到TreeNodes方法(仅通过检查节点的实例)。 TreeNodes的存储箱可以像其他任何存储箱一样被遍历和使用,但是在元素过多的时候,可以提供更快的查找性能。 然而,由于绝大部分正常使用的情况下都不会出现元素过多的情况,因此,查检是否是树节点反而有点儿多余了。

树型节点(例如,所有的存储箱中都存储的TreeNodes元素)主要是基于hashCode来排序的, 但是,假设两个元素都同时实现了Comparable接口,因此,他们的compareTo方法将用于排序,(我们通过反射保守地检查泛型类型来验证这一点——参见方法comparableclassfor), 当键具有不同的散列或可排序时,最坏情况下的的复杂度为o(log n),对树节点增加的额外复杂性是值得的, 因此, 在偶然的情况或者恶意使用返回值不均匀的hashCode方法,很多键将共用同一个哈希值(他们依旧是可排序的),这将带来性能的削弱。(如果这两种方法都不适用,我们可能在时间和空间上浪费大约两倍于不采取预防措施的方式。但是,唯一已知的例子来自于糟糕的用户编程实践,这些实践导致性能已经非常缓慢,以至于看不出什么区别。)

因为TreeNodes比普通节点大两倍,我们只在存储箱中有足够多的元素时才使用(见TREEIFY_THRESHOLD)。随着元素数量的减少(由于删除或者扩容),又会由TreeNodes转换回去。通常情况下,如果使用了足够均匀分布的hashCodes方法,树型节点是很少被使用到的。理想情况下,在随机哈希码时,存储箱中节点的频率遵循泊松分布(http://en.wikipedia.org/wiki/poisson_distribution),默认大小调整阈值为0.75时,平均参数约为0.5,但由于大小调整粒度,变化较大。忽略方差,列表大小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: 小于一千万分之一

树型节点的根通常中他的第一个节点, 然而, 有时(准确的说就是在迭代器中元素被删除的时候)根元素也可能是其它值,但是可以在父链接之后恢复(TreeNode.root()方法)

所有适用的内部方法都接受哈希码作为参数(通常由公共方法提供),允许他们互相调用而不用重计算用户的哈希值。 大部内部方法同样接受一个tab参数, 那通常就是指代的当前哈希表,但也有可能是被扩容或者转换过的哈希表。

当存储箱被树型化,拆分或者反树型化时,我们将它们保持在相同的相对访问/遍历顺序(即Node.next)中,以便更好地保持位置,并稍微简化调用iterator.remove的拆分和遍历的处理。当在插入操作中使用了比较器时,为了在重建平衡时保持总的顺序(或者尽可能接近这里所要求的顺序)不变,我们将类和标识散列码作为连接断路器进行比较。

由于LinkedHashMap子类的存在,普通模式和树模式之间的使用和转换非常复杂。请参阅下面的钩子方法,这些钩子方法定义为在插入、移除和访问时调用,从而允许LinkedHashMap内部保持独立于这些机制。(这还要求将映射实例传递给一些可能创建新节点的实用程序方法。)

基于ssa的并发编程风格有助于避免在所有twisty pointer操作中出现别名混淆错误。

方法与属性

内部静态属性

	/**
     * 默认的初始化容量16,它必须是2的指数倍.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量1073741824,小于等于1<<30. 如果更高的值是由任何一个带参数的构造函数隐式指定的,则使用。 
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认负载因子, 如果在构造函数中没有指定的话,则使用.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 阈值, 用于决定是使用链表存储还是使用树存储。 
     * 当链表中元素个数大于该值是,将转换为树型结构。
     * 在树型结构时,如果元素个数小于该值,并不会直接转换回链表结构,需要参考`UNTREEIFY_THRESHOLD`的值。
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 在树型结构时,如果元素个数小于该值,将转换回链接结构
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 最小的树型化结构容量(否则当存储箱中的容量过多时,就需要扩容), 需要至少4 * TREEIFY_THRESHOLD 以避免扩容与树型化阈值之间的冲突。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

HashMap的普通节点

    /**
     * 基础节点, 用于绝大部分实体的存储。 
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
		...
	}

hash *

    /**
     * 计算`key.hashCode()`的哈希值并将(XORs)高位散列扩展到低位。
	 * 因为该表使用两个掩蔽的能力,所以仅在当前掩蔽之上的位上变化的散列集总是会发生冲突。
	 * (在已知的例子中,有一组浮点键在小表中保存连续的整数)因此我们应用一种转换,将高位的影响向下传播。在效率、实用性和质量之间做了权衡。
	 * 因为许多常见的散列集已经被合理地分配了(所以不受益于传播),而且由于我们使用树来处理容器中的大量冲突,
	 * 所以我们只是以最便宜的方式异或一些移位的位来减少系统损失,以及合并由于表边界而在索引计算中永远不会使用的最高位的影响。
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

comparableClassFor

    /**
     * 如果`x`满足 "class C implements Comparable<C>"则返回`x`的Class,否则返回`null`.
     */
    static Class<?> comparableClassFor(Object x) {
        if (x instanceof Comparable) {
            Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
            if ((c = x.getClass()) == String.class) // bypass checks
                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) &&
                        (as = p.getActualTypeArguments()) != null &&
                        as.length == 1 && as[0] == c) // type arg is c
                        return c;
                }
            }
        }
        return null;
    }

compareComparables

    /**
     * 返回`k.compareTo(x)`的结果(即-1, 0, 1)如果`x`满足`kc` (k是可比较的类),否则返回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 *

    /**
     * 返回一个离给定容量大小最近的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;
    }

这里采用的位运算来获取返回值,之所以要先减一是为了防止给定的容量原本就是一个2的某次幂,从而造成返回的结果是给定容量的两倍。

我们来试一下:

假设置我们给定一个容量为83, 离他最近的一个2的次幂容量为128,依照该方法的代码,依次走一遍。

  1. int n = cap - 1; -> 得到82, 二进制为: 1010010
  2. 开始移位: n |= n >>> 1;
右移一位,再做一次或运算
1010010
 101001
--------
1111011
  1. n |= n >>> 2;
上一步,已经移了一位了,这次在上一步的基础上翻一倍
1111011
  11110
-------
1111111
  1. n |= n >>> 4;
上一步右移了两位,这次向右移四位
1111111
    111
-------
1111111


后续的步骤都可以省略不写了,已经实现所有位置为1了,这也就是最终需要达到的目的。
5. 返回

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

如果小于零的话,就返回1,如果大于等于最大值了,就返回最大值,即1 << 30,否则返回n + 1, 1111111转换为十进制是127, 因此返回结果是128,即2的7次方。

这里我们可以看到,tableSizeFor这个方法其实就是将二进制的所有位都依次变成了1. 假设不进行第一步的减1动作,当我们直接传入128的时候,转换成二进制为10000000, 进行移位和或运算之后,我们将得到结果11111111。最终的返回结果就成了256了。

这可以当作是一个整齐的内存空间,也是为以后能均匀的散列元素而准备的。

内部字段

    /**
     * 存储元素的数组,第一次使用时初始化,在必要的时候扩容,
     * 扩容的长度都是2的倍数,
     * (在某些操作中,我们还允许长度为零,以允许当前不需要的引导机制。)
     */
    transient Node<K,V>[] table;

    /**
     * 保存缓存的`entrySet()`, 在`AbstractMap`中`keySet()`和`values()`会用到
     */
    transient Set<Map.Entry<K,V>> entrySet;

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

    /**
     * `HashMap`被结构化数据修改的次数, 会影响到键值对数量的修改或者内部结构发生变化的修改(比如重建hash)
     * 这个数据主要和于迭代时的快速失败。
     */
    transient int modCount;

    /**
     * 下一次扩容的阈值 (容量 * 负载因子).
     * 另外,如果存储元素的数组还未进行过分配,这个值就是初始容量,或者零或者最大值。
     */
    int threshold;

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

构造方法

    /**
     * 指定初始化容量和负载因子的空`HashMap`
     */
    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`
     * 采用默认的负载因子(0.75).
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
	 * 空`HashMap`
     * 采用默认的负载因子(0.75).
     * 采用默认的容量(16)..
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    /**
     * 以默认的负载因子和以m的大小且合适的容量创建一个`HashMap`, 
     * 将一个继承自`MAP`接口的键值对放到哈希表中
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

putMapEntries *

    /**
     * 把键值对实体放入`HashMap`中
     *
     * @param m 待放入的键值对
     * @param evict 初始化的时候是false, 
     * 其它情况都是true,这个参数主要是为`LinkedHashMap`服务的
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            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);
            }
        }
    }

如果当前用于存储元素的数组还不存在,由会根据负载因子和当前待放入的键值对大小来计算出一个初始容量,这里就使用到了tableSizeFor来计算容量。

如果数组已经存在,这时threshold就是扩容的阈值,当超过阈值时,就需要进行扩容操作,调用resize()

使用增强for循环将值放入数组中。(数组的初始化操作将在resize()中完成)

size

    /**
     * 返回`HashMap`中键值对个数.
     */
    public int size() {
        return size;
    }

isEmpty

    /**
     * 判断`HashMap`是否为空.
     */
    public boolean isEmpty() {
        return size == 0;
    }

get

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

getNode *

    /**
     * Map.get 的具体实现
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            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;
    }

在第一个if中,把赋值和判断放在了一起,我们先把代码拆分开来看:

1   tab = table;
2   n = tab.length;
3   first = tab[(n - 1) & hash]
4   if (tab != null && n > 0 && first != null) { ... }

前两行代码没什么好说有,第三行获取元素这里需要强调一下,我们先做出如下假设:

  • 哈希表的容量为128;
  • 我们想要获取的元素为A;
  • 这个值存在

first是如何取出的呢?我们一步一步的来走

  1. n - 1

已知n = 128, 那n - 1 = 127, 转化为二进制1111111备用。
2. (n - 1) & hash

在上一个方法get中,获取值使用的是getNode(hash(key), key)),因此,hash = hash("A"),结果为65, 转化为二进制是1000001;

1111111
1000001
-------
1000001

取出第一个节点tab[65].

比较有意思的就是在这里了。

我们现在使用的是一个单个字符,相对比较短,如果key值是AAAAAhash('AAAAA')得到62030771,转为二进制11101100101000001110110011, 再进行&操作:

11101100101000001110110011
                   1111111
--------------------------
00000000000000000000110011

得到的结果是0110011, 转化为十进制即51, 也就是说这个值的存和取都在tab[51]

试想,如果这不是一个2的某次幂的容量,假设容量是10, n - 1之后,再转也二进制,结果是1001, 在做&操作时,中间两位始终都是零, 我们随意举三个示例:

101010    101110    11000
  1001      1001     1001
------    ------    -----
  1000      1000     1000

虽然三个键对应的hash值不一样,但是,最终在计算存储位置的时候,却都计算出了同一个结果, 让数据有侧重点的偏向了一头一尾的位置,使得头尾都产生了很长的链表或者树,而中间的区域却是空着的。

因此,以2的某次幂作为哈希表的容量是有利于元素均匀散列的,如果直接使用键自己的hash值,那也仅仅是低位区间影响到元素的存放位置,在HashMap中使用自己的hash方法使高位区间和低位区间互相影响,据查可以对均匀散列的能力提高10%,暂未验证。

回到主题,取出节点就意味着能拿到值吗?

我们接着来看取值的过程:

  1. if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))

代码里有一行注释// always check first node, 因为,如果第一个值就是目标值了,那还用得着去判断这个节点是一个单个元素,还是一个链表或者一个红黑树吗?在过去的知识中,我们知道hashequals的关系是,如果hash值一致,那equals不一定返回true, 如果equals返回true,那hash值必然一致; ==号判断的两个对象的内存地址是否一致;这里的取值正是应用了这两个知识。
2. if ((e = first.next) != null)

如果上一步没有返回出去的话,那就要看一下这个节点是否是一个链表或者树了,如果是一个树,那就按照树的方式去取值((TreeNode<K,V>)first).getTreeNode(hash, key), 如果不是树的话,那就是链表,那就next一路到末尾的节点进行依次判断取值。

containsKey

    /**
     * 如果哈希表中存在指定的键,则返回true
     */
    public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }

都是通过getNode(hash(key), key)来获取一个节点,但在get方法中,获取到了节点之后,判断是否为null,是,则返回null,不是,则返回节点的值。如果节点的值本身就是一个null,则根本就不知道到底是因为节点不存在,还是节点本身为空而返回出来的nullcontainsKey则可以根据节点是否为空来判断键是否存在。

put

    /**
     * 将指定的键和值关联到哈希表.
     * 如果哈希表中已经存在了指定键的值,那原值将被替换
     *
     * @param key 指定值的键
     * @param value 指定键的值
     * @return 指定键原来关联的值,如果指定的键原来没有值那就返回null.
     *         (返回null,可能是原来没有任何相关联的值,也可能是原来关联的值本身就是null)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal *

    /**
     * Map.put 的具体实现类
     *
     * @param hash 键的hash值
     * @param key 键
     * @param value 值
     * @param onlyIfAbsent 如果true, 不改变旧值
     * @param evict 如果false, 进入创建模式.
     * @return 如果有旧值,返回旧值,如果没有,返回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;
        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);
        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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, 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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

顺序往下读:

先初始化一些必要参数, 如果table是空的或者没有任何初始化的大小,则进行一次resize()操作, 并重新为tabn赋一次值, resize()后面再来看是做了什么事儿。

p = tab[i = (n - 1) & hash]这里和getNode里获取first是一样的动作, 如果这里是空的,那就直接创建一个新的节点放进去, put就完事了, newNode里做了什么后面再说。

如果发生了哈希碰撞,这个节点上就不会只有一个值, 那就需要在这个节点上生成链表或者树。

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))这也和getNode里的代码长得很像, put放入元素的键要判断是否已经存在,如果存在,就覆盖这个键的值,并且把旧值返回回去, 这里就是先判断第一个节点的键是否相等。

else if (p instanceof TreeNode), 如果是红黑树,则putTreeVal, 后续再说这是怎么做的。

最后是处理链表:

	// 链表实际长度不知道,得一个一个的执行`next`来获取, 通常是通过`hasNext`来判断是否到末尾了, 比如这里写成`while(p.hasNext())`, 但是这里需要有一个计数器来控制链表和红黑树之间的转换, 在计数器达到`TREEIFY_THRESHOLD`(减1是因为计数器以零开始)之后,就有一个转化过程
    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);
            break;
        }
		// 如果找到了和当前键一样的节点,则直接停止循环, 注意: 上一步已经做了e = p.next这个动作了。
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            break;
		// 有当前循环内如果什么都没有做,那就接着下一个节点赋值给p
        p = e;
    }

如果有旧值存在,则取出旧值,可以修改旧值,则修改,然后,将旧值返回出去。

元素放进去之后,再对modCount加一,然后,判断是否到了的扩容阈值,如果达到了,进行resize()操作。

方法中的afterNodeAccess(e)afterNodeInsertion(evict)都是为LinkedHashMap准备的。

resize *

    /**
     * 对哈希表进行初始化或者扩容。 
     * 如果是`null`, 则以`threshold`为初始容量分配空间。 
     * 否则,由于哈希表的容量是2的某次幂,因此,所有的元素要么保持原有位置不动,要么在扩容的新哈希表中偏移2的幂次的位置。
     */
      final Node<K,V>[] resize() {
1         Node<K,V>[] oldTab = table;
2         int oldCap = (oldTab == null) ? 0 : oldTab.length;
3         int oldThr = threshold;
4         int newCap, newThr = 0;
5         if (oldCap > 0) {
6             if (oldCap >= MAXIMUM_CAPACITY) {
7                 threshold = Integer.MAX_VALUE;
8                 return oldTab;
9             }
10            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
11                     oldCap >= DEFAULT_INITIAL_CAPACITY)
12                newThr = oldThr << 1; // double threshold
13        }
14        else if (oldThr > 0) // initial capacity was placed in threshold
15            newCap = oldThr;
16        else {               // zero initial threshold signifies using defaults
17            newCap = DEFAULT_INITIAL_CAPACITY;
18            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
19        }
20        if (newThr == 0) {
21            float ft = (float)newCap * loadFactor;
22            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
23                      (int)ft : Integer.MAX_VALUE);
24        }
25        threshold = newThr;
26        @SuppressWarnings({"rawtypes","unchecked"})
27        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
28        table = newTab;
29        if (oldTab != null) {
30            for (int j = 0; j < oldCap; ++j) {
31                Node<K,V> e;
32                if ((e = oldTab[j]) != null) {
33                    oldTab[j] = null;
34                    if (e.next == null)
35                        newTab[e.hash & (newCap - 1)] = e;
36                    else if (e instanceof TreeNode)
36                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
37                    else { // preserve order
38                        Node<K,V> loHead = null, loTail = null;
39                        Node<K,V> hiHead = null, hiTail = null;
40                        Node<K,V> next;
41                        do {
42                            next = e.next;
43                            if ((e.hash & oldCap) == 0) {
44                                if (loTail == null)
45                                    loHead = e;
46                                else
47                                    loTail.next = e;
48                                loTail = e;
49                            }
50                            else {
51                                if (hiTail == null)
52                                    hiHead = e;
53                                else
54                                    hiTail.next = e;
55                                hiTail = e;
56                            }
57                        } while ((e = next) != null);
58                        if (loTail != null) {
59                            loTail.next = null;
60                            newTab[j] = loHead;
61                        }
62                        if (hiTail != null) {
63                            hiTail.next = null;
64                            newTab[j + oldCap] = hiHead;
65                        }
66                    }
67                }
68            }
69        }
70        return newTab;
      }

前文我们提到过,HashMap的初始化是在resize()中完成的,因此,一旦有元素开始要放入到哈希表中,初始化动作就开始了。我们先以插入第一个元素前的次初始化为示例来看看resize()干了什么。

在这里,我们并不会考虑使用带参数的构造函数

由于tablenull, threshold0, 因此,会直接走到第16行,使用默认值计算出容量和阈值。

newCap = DEFAULT_INITIAL_CAPACITY;, 即16;

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);, 即0.75 * 16 = 12, 如果有小数的话,会被类型强制转换而取整(实际这种情况几乎不会发生,除非你使用2作为初始容量);

然后在27行初始化出一个容量为16的数组。

由于table还是空的,29行往下就不考虑了,将初始化好的哈希表返回出去。

默认情况下,当容量达到12之后,第13的元素的插入将超过阈值。

那到底是第12个元素插入之后就开始扩容,还是第13个元素插入之后再扩容的呢?

又回到putVal这个方法的末尾逻辑:

        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);

如果++size大于threshold将进行扩容。size的初始值是0, 而++size又是先进行+操作,再进行与threshold的比较,在第13个元素插入完成之后,这个if判断才会成立。所以,是在第13个元素插入完成之后再进行的扩容操作。

好了,现在来看看扩容干了什么。

这次的情况与初始化哈希表时是不同的了,oldCapoldThr都是有值的,分别为16和12. 在第6到12行将判断扩容后的容量是否会超过容量上限,超过了的话,将直接返回容量上限, 对于哈希表的第一次扩容是完全没问题的,这里使用了左移一位的方式对原有的容量进行翻倍。

    newCap = oldCap << 1; // 即32
    newThr = oldThr << 1; // 即24

在第27行就出现了一个新的哈希表,容量为32。

新的哈希表有了,所有的元素还在oldTab里,现在得把他们全部都挪过去, 第29行开始就全部都是挪元素的过程。

哈希表的每一个节点包含如下字段:

    final int hash;
    final K key;
    V value;
    Node<K,V> next;

所以,不用再去hash(Object key), 而是直接使得节点里的hash即可。

现在节点的元素是有了,但应该放到新表的哪里呢?

由于有一些特别的问题需要说明,同时也方便与之前的示例对比,所以,我们还是以如下假设来进行举例:

  • 扩容前的容量是128, 扩容后为256
  • AAAAAA都存储在哈希表中
  1. 如果这是一个单个节点元素,即e.next == null

执行newTab[e.hash & (newCap - 1)] = e;, 还是以之前存储过的A为例,现在的e.hash & (newCap - 1)如下:

 1000001
11111111
--------
 1000001

耶,还是65, 位置不变, 新的哈希表中,A还是存储在老位置。

再来看看之前示例中的另一个键AAAAA:

11101100101000001110110011
                  11111111
--------------------------
00000000000000000010110011

这次的结果被容量的变化影响到了,地址变成了10110011, 转换为十进制179,与原来的51相比偏移了128,偏移了2的7次方。

这里需要特别说明一下,不是因为键的hash值比较小才造成位置不变的,能肯定的只是但凡键的hash值小于原哈希表容量的位置都会不变,而一些键的hash比较大的也可能会位置不变,比如:

1000000000010011    1000000000010011    1000000000010011    1000000000010011
        11111111           111111111           111111111          1111111111
----------------    ----------------    ----------------    ----------------
0000000000010011    0000000000010011    0000000000010011    0000000000010011

在前期的多次扩容中,它都会被放在tab[19]这个位置上。
2. 如果是一个红黑树e instanceof TreeNode,则按照树的方式进行拆分重新存储
3. 如果是一个链表, 链表也会被拆分开, 以怎样的方式来拆呢?

对于我们示例的扩容前容量128, 我们可以有如下两个结论:

  • 重新计算存储位置后保持不变
  • 重新计算存储位置后相比原有位置偏移了128

那一个链表中, 也会同时存在如上两类不同的元素, 拆分的目的就是形成两个链表, 一个保持不变,另一个放到偏移后的位置上. 在3839这两行代码中, Node<K,V> loHead = null, loTail = null;将存储保持不变的元素, Node<K,V> hiHead = null, hiTail = null;将存储发生了偏移的元素.

resize这个方法中是用的什么方法来判断哪些不变,哪些会发生偏移呢?

43行代码(e.hash & oldCap) == 0就是一种非常不错的位运算判断方式.

128这个容量的转换成二进制是10000000, 还是来看AAAAA:

11101100101000001110110011
                  10000000
--------------------------
00000000000000000010000000

得到10000000, 即会发生偏移128. 在计算元素存储位置时, 使用的是1111111, 如果想要在扩容后(即11111111)不被影响, 那么键的hash中倒数第八位必须是0, 否则的话,就一定会发生偏移. 看到这里,我们就知道了在容量128的情况下, 以倒数第八位的值就可以拆分出两个链表, (e.hash & oldCap) == 0这个条件完美解决问题.

拆分出链表之后,将保持不变的放在当前位置,将发生偏移的放到放到当前位置加上偏移量的位置上.

treeifyBin

    /**
     * 根据给定的hash值,把链表转化为树型结构
     * 如果哈希表太小的话, 则只进行扩容.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        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);
                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);
        }
    }

MIN_TREEIFY_CAPACITY设置一个64是出于一个什么考虑呢? 在低于这个值的时候, 选择了使用扩容的方式, 我想是由于低容量下, 发生哈希碰撞的机率更大些, 与此同时, 也会很快就达到下一个扩容的阈值, 所以这个时候,选择扩容,将长链表拆开所产生的性能消耗比树型化更低, 就算树型化了, 达到下一个扩容阈值的时候, 也有很大的可能性被拆回到链表, 因此设置一个阈值是很有必要的.

在满足树型化条件的情况下, treeifyBin方法也主要只是做了将原有的Node<K,V>节点转化成了TreeNode<K,V>, 最多算是一棵二叉树, 真正的红黑树转化是在方法最后的hd.treeify(tab)中.

putAll

    /**
     * 将指定MAP中的键值对复制到当前哈希表中, 如果有相同的键,将被新的值覆盖.
     */
    public void putAll(Map<? extends K, ? extends V> m) {
        putMapEntries(m, true);
    }

remove

    /**
     * 如果存在的话,则从当前哈希表中删除指定的键值对.
     *
     * @param  key 待删除的键
     * @return 该键对应的值, 如果没有找到的话则返回null.
     * (返回null值并不代表这个键不存在, 也可能是这个键对应的值就是一个null)
     */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

removeNode *

    /**
     * Map.remove 的具体实现类
     *
     * @param hash 键的hash
     * @param key 键
     * @param value 如果matchValue是true的话, 则需要匹配值, 否则忽略
     * @param matchValue 如果为true的话,则需要匹配值
     * @param movable 在树型结构中是是否移动节点
     * @return 当前节点或者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;
        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)
                    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;
    }

删除节点是比较常规的操作了, 首先是获取到这个节点,然后根据valuematchValue对获取到的节点进行操作。

如果是红黑树,则调用removeTreeNode

如果(node == p), 则第一个节点就是需要删除的节点,需要把node的next放到哈希表的相应数据位中去(如果是单个元素的话,node.next也正好就是null,相当于数据位被置空了);
其它情况下,则是一个链表, 替换一个next即可。

这里比较不和谐的是获取节点的方式

我们把getNoderemoveNode中获取节点的代码对比一下:

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

这两坨除了删除操作中的else if (node == p)这句用到的变量p,其它都是一样的, why这么写?

clear

    /**
     * 清空哈希表.
     */
    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

    /**
     * 如果哈希表中包含指定的值,则返回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;
    }

keySet, values, entrySet *

为什么要把这三个放到一起,因为这三个是同一种实现方式;

  • keySet: 得到的是所有键的Set集合;
  • values: 得到的是所有值的Collection集合;
  • entrySet: 得到的是所有键值对的Set集合;
    在这里我们仅以keySet为例来解读, 剩下的两个可以触类旁通。
    /**
     * 返回哈希表中所有键的集合。
     * 这个集合只是一个引用,因此,对哈希表的修改都将反馈在这个集合上,反之亦然。
     * 在集合迭代过程中,如果哈希表被修改了(除非是通过自身迭代器的`remove`操作), 迭代结果将会是未定义的。
     * 集合的迭代器支持删除操作, 删除哈希表的键可以通过`Iterator.remove`, `Set.remove`, `removeAll`, `retainAll`, `clear`.
     * 但是不支持`add`或者`addAll`
     */
    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

代码只有区区六行, 最重要的代码只有一行ks = new KeySet(), 按照我们通常的逻辑, 要拿到所有键的集合,好歹也要遍历一下table, 再把所有的键都放到Set中, 将这个Set返回出去才对啊。 别急, 我们先来看一下以下代码,看看会打印什么出来!

     Map<String, String> map = new HashMap<>();

     map.put("KEY_1", "VALUE_1");
     map.put("KEY_2", "VALUE_2");

     Set<String> keySets = map.keySet();
1    System.out.println(keySets);
     map.put("KEY_3", "VALUE_3");
2    System.out.println(keySets);

在标记的两个打印位置上,会打印出什么结果, 你也可以复制出去执行一下,打印的是如下结果:

[KEY_1, KEY_2]
[KEY_1, KEY_3, KEY_2]

如果HashMap按照我们自己的思路来走的话,那行号标记1处打印两个键,在没有重新获取keySet()的情况下,行号标记2处也只会打印两个键。

再来看一下KeySet的具体实现:

    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> 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.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

这是一个集合的实现方式,JDK中的集合几乎都是以这种方式在实现, 在打印时调用的toString是在AbstractCollection中被重写了的, 重写的方法如下:

    /**
     * 返回这个集合的字符器对象.  
     * 字符器的顺序与迭代器从集合中取得的顺序一致,并用方括号("[]")括起来。
     * 相邻元素使用", "分隔(一个","和一个空格)。
     * 元素使用String.valueOf(Object)转换成字符串.
     */
    public String toString() {
        Iterator<E> it = iterator();
        if (! it.hasNext())
            return "[]";

        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(this Collection)" : e);
            if (! it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }

toString中是通过iterator()来获取的集合元素, iterator()是一个定义在AbstractCollection抽象方法public abstract Iterator<E> iterator();,在KeySet中的实现如下:

    public final Iterator<K> iterator() { 
        return new KeyIterator();
    }

这里又返回了一个实例KeyIterator, 接着看代码:

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

到这里,总算是知道在执行toString的时候, E e = it.next();具体取的是什么值了, 是来自于HashIteratornextNode方法返回节点中的key值, HashIterator的代码如下:

abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

这里面实现的的方法也仅仅只有hasNext, remove, 还有一个自己的方法nextNode, 所以,在Iterator接口中,只有next方法是被单独实现的。

就近的,这里罗列了三个类似的类:

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
        implements Iterator<V> {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

也就是说, 在共用迭代器的时候, 返回不同的nextNode()属性或者nextNode()自身,就可以把valuesentrySet给实现了。

我们在举例的时候,也仅仅是说了toString中迭代器的具体调用过程, 实际上,你可以看到,在AbstractCollection中, 应用到该迭代器的还包括contains(Object o), toArray(), toArray(T[] a), remove(Object o), removeAll(Collection<?> c), retainAll(Collection<?> c), clear()这些都是集合基类中的标准实现, 所有的集合类都在这些特性。

KeySet中,并没有对add的相关方法进行实现, 所以,在调用时都会直接抛出异常throw new UnsupportedOperationException();.

getOrDefault

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

获取值的时候, 如果没有找到指定键的话,就返回默认值defaultValue.

putIfAbsent

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

插入键值对的时候,如果旧值是null, 则替换, 否则保持不变。

remove

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

删除动作, 在removeNode中查看具体实现。

replace

    public boolean replace(K key, V oldValue, V newValue) {
        Node<K,V> e; V v;
        if ((e = getNode(hash(key), key)) != null &&
            ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }

对指定键所映射的值的替换, 如果指定键所映射值与oldValue相等,则远的为newValue.

replace

    public V replace(K key, V value) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }

对指定键所映射的值的替换, 如果能找到指定的键,则替换其值。

computeIfAbsent

    public V computeIfAbsent(K key,
                             Function<? super K, ? extends V> mappingFunction) {
        if (mappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
            V oldValue;
            if (old != null && (oldValue = old.value) != null) {
                afterNodeAccess(old);
                return oldValue;
            }
        }
        V v = mappingFunction.apply(key);
        if (v == null) {
            return null;
        } else if (old != null) {
            old.value = v;
            afterNodeAccess(old);
            return v;
        }
        else if (t != null)
            t.putTreeVal(this, tab, hash, key, v);
        else {
            tab[i] = newNode(hash, key, v, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
        return v;
    }

这里的绝大多数代码都已经在前面的提到过了,就不再赘述, 直接说它的作用吧!

在哈希表中去查找指定键的值, 如果找到了,并且值不是null, 就直接把这个值返回回去,如果没有找到,则执行V v = mappingFunction.apply(key), 执行结果如果不是空, 并且旧值存在但值是空,则替换原来的空值,如果旧值不存在,则创建一个新的键值对, 并将执行结果返回回去。

newNode

Node节点是一个内部类, 创建新节点new Node<>(hash, key, value, next)使用的构造方法是:

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

节点中是直接带了hash值的, 应该是为了防止以后要使用的时候不必要的重复计算而放入节点中的,在key固定的情况下,它也变不了。
在节点中, 判断节点是否相等的hashCodeequals都是重写了的:

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

hash(Object key)不同的是, hash方法是自身hashCode与其低十六位进行导或, 节点中的hashCode是键和值进行直接异或。

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

至此,后续的类和方法也就没有必要再多费时解读了,主要的类也就是红黑树及一些树的操作,HashMap的重要内容也就到此结束。

接下来,抽取几个相对重要的内部类方法。

内部类方法

TreeNode 中的 putTreeVal

      /**
       * 红黑树版的`putVal`.
       */
      final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                     int h, K k, V v) {
1         Class<?> kc = null;
2         boolean searched = false;
3         TreeNode<K,V> root = (parent != null) ? root() : this;
4         for (TreeNode<K,V> p = root;;) {
5             int dir, ph; K pk;
6             if ((ph = p.hash) > h)
7                 dir = -1;
8             else if (ph < h)
9                 dir = 1;
10            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
11                return p;
12            else if ((kc == null &&
13                      (kc = comparableClassFor(k)) == null) ||
14                     (dir = compareComparables(kc, k, pk)) == 0) {
15                if (!searched) {
16                    TreeNode<K,V> q, ch;
17                    searched = true;
18                    if (((ch = p.left) != null &&
19                         (q = ch.find(h, k, kc)) != null) ||
20                        ((ch = p.right) != null &&
21                         (q = ch.find(h, k, kc)) != null))
22                        return q;
23                }
24                dir = tieBreakOrder(k, pk);
25            }
  
26            TreeNode<K,V> xp = p;
27            if ((p = (dir <= 0) ? p.left : p.right) == null) {
28                Node<K,V> xpn = xp.next;
29                TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
30                if (dir <= 0)
31                    xp.left = x;
32                else
33                    xp.right = x;
34                xp.next = x;
35                x.parent = x.prev = xp;
36                if (xpn != null)
37                    ((TreeNode<K,V>)xpn).prev = x;
38                moveRootToFront(tab, balanceInsertion(root, x));
39                return null;
              }
          }
      }

HashMap采用了红黑树来存储长度超过8的链表数据, 红黑树除了是自平衡二叉树,还有以下特性:

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  4. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

带着这些特性,我们来看一下树版的putVal

在存入一个新的值到一个已经存在的红黑树时,首先需要找到这个树的根在哪里, (parent != null) ? root() : this即可获取出根。

从第4行开始,这是一个死循环的入口,第一次是与根比较,通过与根做比较,我们才会知道新的节点是放到树的左边还是右边,如果相等的话,那就直接返回了,第12行则是判断中比较特殊的情况, 又发生了哈希碰撞, 这个判断拆开来看看:

kc = comparableClassFor(k);
dir = compareComparables(kc, k, pk);

这里使用到了最开头的两个类comparableClassForcompareComparables, 判断中的条件是如果kc原本就是空,且当前这个key是一个不可比较的类, 或者这个key是可以比较的,但根据比较的条件也判断他们相等, 则继续进入if语句中, 否则的话,比较的结果(compareTo的比较结果是-1, 0, 1, 零表示相等)放入到dir中。 在进入到if中会有一个searched的判断条件, 这个只会执行一次, 将当前这个p节点的左支和右支都过一遍, 如果找到了key相等的,就返回,如果没有,继续执行第24行代码dir = tieBreakOrder(k, pk), 这里就有点儿耍流氓了。

其实计算到了这里,已经是极低概率发生的事情了,那得是什么情况才会走到这里来(也或者代码得太烂,跑到这里来Debug了)。

我们还是来猫一眼这种极端情况。

tieBreakOrder中写了啥:

    static int tieBreakOrder(Object a, Object b) {
        int d;
        if (a == null || b == null ||
            (d = a.getClass().getName().
             compareTo(b.getClass().getName())) == 0)
            d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                 -1 : 1);
        return d;
    }

这里检查了啥呢?把两个对象的类名拿出来比较,如果类名还是相等的,那就开始我前面说的耍流氓。System.identityHashCode已经是一个native的方法了, 方法上说这个方法将按照默认的hashCode来返回结果,无论是否被重要了hashCode, 那默认的hashCode是返回的什么呢?方法上的注释是这么说的:

As much as is reasonably practical, the hashCode method defined by class {@code Object} does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.)

这通常是通过将对象的内部地址转换为整数,但这并不是由Java语言来实现的。

这里的判断方式是(System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1)因此,哪怕是一样的,也要强行区分一个大小, 只要走投无路,调用了tieBreakOrder就一定会返回一个非零的值。

这个else if之后,再无else,如果真有,那dir还真有可能会变成默认值的0

接下来,根据dir的大小,来判断是走左分支还是右分支, 如果为空的话,那这里就将是一个新值的插入点,然后对树进行平衡操作。如果不为空的话,那就继续走循环。

最后,插入成功则返回null,找到了相应的key则返回相应的节点,旧值的替换将在putVal中完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值