HashMap(JDK1.8)

前言知识

哈希化

用位运算实现哈希化

我们知道HashMap是基于哈希表实现的,而且是链地址法实现的哈希表(即数组加链表的形式)。

哈希表的关键是哈希化,就是将很大的哈希值转化变成一定区间范围内的值,我们能够马上想到的就是采用取余%操作来实现哈希化的。但是我们知道取余%是非常耗费性能的,尽量不要使用这种方式,那怎么实现高效率的哈希化呢?

我们知道两个数最快的运算方式是位运算,那么怎么样使用位运算将较大的哈希值hashCode变成一定区间范围内的值呢?

那就使用&运算。我们知道如果有一个数它的高位都是0,低位都是1,即0b000011111...这种形式。它与任何一个数进行&运算,得到的结果值都不可能大于这个数。例如0b111 & 0b1010111010010 = 0b010。因为&运算只有两位都是1才是1,否则就是0。

那怎么得到0b000011111...这种形式的数?

有一个非常简单的方法,如果一个数是2的幂数,即0b0001000...这种形式,例如 2、4、8、16、32等等。这个数减一得到的数就是0b000011111...这种形式。例如8-1 =7(0b111)。

这个就解释了为什么HashMap数组的长度必须是2的幂数,因为这样就可以使用&运算的方式实现哈希化。即 (table.length - 1) & hashCode。

这个也是一个公式,即 任何一个数x % 2^n都可以转成 数x & (2^n - 1)。也就是说数x对2^n进行取余,本质上是返回数x 低n位二进制数。这个很重要,对我们理解resize()方法由很大帮助,具体请看后面对resize()方法的详解。

哈希值高位失效的问题

这里还是有个问题,那就是哈希值hashCode高位失效。

因为&运算导致哈希值hashCode的高位不管是0或者1,与0相与结果都是0,会导致很多不同的哈希值hashCode只要低位是相等的,那么哈希化(即相与操作)得到的值是一样的。

它们会存到同一个链表中,导致哈希表中有的数组存放的链表很长,有的却是为空,很影响哈希表的效率。

怎么处理hashCode高位失效问题呢?就要用到HashMap中一个静态方法int hash(Object key)。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

看到这种表达式,一定很懵逼,一步一步分析它的作用。

先看一下它是怎么处理的,h表示key值的哈希值。h >>> 16表示右移16位,我们知道一个int整形有32位,右移16位表示丢弃高16位的数据,因为都变成高16位都变成0了,然后再和原来的h值进行异或^。

要知道一个事实:就是异或^0,每一位保持不变。0b101 ^ 0b000 = 0b101。h ^ (h >>> 16)得到的结果我们知道高16位是不变的,低16位才可能发生改变,而且它的改变是与高16位数有关的,这样就将高16位的数据也利用起来,减缓了高位失效。

数组长度

根据上一节我们知道,HashMap中数组的长度必须是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;
}

我们知道当哈希表元素的个数超出哈希表阈值(即数组长度*负载因子),就要进行数组扩容,而根据上一节我们知道,HashMap中数组的长度必须是2的幂数。

所以这个方法就是返回一个与cap最近的2的幂数。

怎样返回一个与cap最近的2的幂数呢?有一个简单地方法。
int n = 1; while (n < cap) n <<= 1;
这个实现方式很容易理解,因为n开始值是1,而n <<= 1得到的值一定也是2的幂数,然后利用循环,找出比cap大(包括等于)的数。
这种方式简单易懂,以前HashMap就是用这种方式计算的,但是可能觉得两个数判断大小很耗时间或者循环比较耗时间,所以使用了上面这种全新的算法。、

上面这种算法理解起来就比较麻烦了。

  • 首先我们确定一个事实,一个不是0正整数,它可以表示0b0001XXX这种形式,我们就用0b1000做示范。
  • 第一步 n |= n >>> 1 即 0b1000 | 0b0100 = 0b1100
  • 第二步 n |= n >>> 2 即 0b1100 | 0b0011 = 0b1111
  • 第三步 n |= n >>> 4 即 0b1111 | 0b0000 = 0b1111

不知道大家看出来什么规律了么?

  • 第一步将n右移一位,再与n相或,它的作用就是将n从0b0001XXX变成0b00011XX形式。
  • 再看第二步,n右移两位,那么就是将n从0b00011XX变成0b0001111形式。
  • 第三步时,发现值没有变化,那是因为我们已经将n完成转换成0b0001111...形式。
  • 因为int是32位的,所以需要n |= n >>> 16才能确保覆盖整个整数范围,最后我们再使用n = n+1,将n从0b0001XXX变成0b0010000形式(也就是2的幂数)

还有个疑惑,这里为什么要n = cap - 1,因为按照我们的逻辑,直接使用cap也可以啊。

这就考虑的一个边界问题了,如果cap就是2的幂数,比如是8(即0b1000),按照算法我们将它变成0b1111形式,最后再加1,就变成了16(即0b10000),那么就就对了,因为与8最近2的幂数应该就是8,与9最近2的幂数才是16。所以这里先cap - 1将8变成7(0b111),最后结果还是8。

最后n >= MAXIMUM_CAPACITY判断条件,是因为HashMap 数组最大长度是1 << 30,所以要判断最大值。

数组扩容

数组扩容对于基于数组实现的集合都是很重要的方法,比如ArrayList和HashMap。而HashMap的数组扩容就更加麻烦,它涉及到再哈希的问题。

因为扩容后的数组长度与原来数组长度是不一样的,那么哈希化之后得到的下标值也有可能是不一样的。

再哈希问题新算法

HashMap的数组扩容是通过resize()方法实现的,这个方法最重要的就是将老数组中全部键值对存放到新数组中。也就是再哈希问题。

一种简单的方法,就是先遍历老数组,获取对应链表,再遍历链表,得到每个键值对,然后对键值对的key进行新数组的哈希化,得到一个下标值,然后进行处理。看一看以前版本jdk是怎么处理这个问题的。

// jdk老版本进行重新哈希化的算法
// 将HashMap中的全部元素都添加到newTable中
void transfer(Entry[] newTable) {
    // HashMap老的哈希表
    Entry[] src = table;
    // 新的哈希表长度
    int newCapacity = newTable.length;
    // 遍历老的哈希表
    for (int j = 0; j < src.length; j++) {
        // 得到j下标处的链表
        Entry<K, V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K, V> next = e.next;
                // 得到新的哈希化的下标值
                int i = indexFor(e.hash, newCapacity);
                // 将元素e插入到链表的表头
                e.next = newTable[i];
                newTable[i] = e;
                // 将next赋值给e,遍历老的链表
                e = next;
            } while (e != null);
        }
    }
}

老版本代码逻辑比较简单:

  • 遍历老的哈希表,得到链表头元素e,以及下一个元素next,并求出新的哈希化下标值i。
  • 将e插入新的哈希表i下标位置链表的表头。通过公式e.next = newTable[i],newTable[i] = e来实现
  • 将next赋值给e,循环遍历老的链表

这种实现方式简单明了,容易让人理解。但是有个问题,就是得到的新链表和老链表是反向的,因为我们遍历老链表是从头到尾的,但是插入新链表却是从头插入。

jdk1.8对再哈希问题提供了全新的算法。我们知道HashMap集合数组长度必须是2的幂数(2^n), 而且每次扩容老数组的一倍,即2^(n+1)。

  • 集合中数组数量是2的幂数2^n即0b00010000..这种形式,进行哈希化,就是用一个数x & (2^n - 1) 返回的就是数x低位的二进制数(即低n位的二进制数)

例如 长度是4(2^2) 0b00100,那么3(2^2 - 1) 0b00011。
数x分别是3 0b00011 7 0b00111 15 0b01111,它们 & 3 得到都是3,也就是它们在低位(低2位)的表现形式。

  • HashMap集合进行数组扩容时,得到新数组长度就是2^(n+1),对新数组进行哈希化,就变成了 数x & (2^ (n + 1) - 1), 返回的是数x低n+1位的二进制数。

这个时候我们发现与扩容前相比,哈希化的下标值低n位是不变的,有可能改变的就是第n+1位(从低往高数)。

  • 如果第n+1位是0,那么新的哈希化的得到的下标值和原来的是一样的。
  • 如果第n+1位是1,那么新得到的下标值与原来相比,要加上 2^n (即原数组长度)。
例如 长度就从4(2^2) 变成了8(2^3) 0b01000, 那么7(2^3 - 1) 0b00111。
数x分别是3 0b00011 7 0b00111 15 0b01111,它们 & 7 得到的分别是 3,7, 7,也就是它们在低位(低3位)的表现形式。
 
注意 第n+1位对应的数是2^n, 比如说第3(2+1)位对应的数是4(2^2),即0b100。

因此我们发现对新数组进行哈希化,有了更好的方式,不需要使用数x & (2^(n+1) - 1)得到新的下标值。变成判断数x的第n+1位(从低往高数)是0还是1。
判断数x的第n+1位是0还是1,也有个很简单的方式。就是用数x & 2^n ,因为2^n 只有第n+1位是1,其他位置都是0。那么它与任何数相&只有两个结果,0表示数x第n+1是0,1表示数x第n+1位是1,而2^n就是老数组的长度。

再哈希新算法代码实现

先遍历老数组,用j表示数组下标,获取下标j对应位置的链表e,如果链表e不为null,那么下标j就是链表e中键值对对应的哈希值。

根据链表中元素的数量分为两种情况:

1、链表元素为1

if (e.next == null)  newTab[e.hash & (newCap - 1)] = e;

当e.next为null,那么链表只有一个元素。发现这里使用了e.hash & (newCap - 1)计算新的下标值,然后直接将e赋值到这个位置。这时我们就有两个疑惑了。

(1)为什么没有用新的方式计算下标值呢?这里其实是可以用新的计算方式的,例如:

int newIndex = (e.hash & oldCap) == 0 ? j : j + oldCap;
newTab[newIndex] = e;

这个得到的newIndex值与e.hash & (newCap - 1)得到的值是一样的,而且计算起来更加麻烦,这里就不用这种方式。

(2)为什么直接将e赋值给newTab数组,难道不怕newTab数组该位置已经有了元素,而导致被覆盖情况么?

既然源码这么写的,那么就不可能出现覆盖情况,这是为什么呢?
我们说过新数组和老数组哈希化得到的值只有一个位会可能不一样,也就是说新数组哈希化得到的下标值,要么与老数组哈希化得到的值一样,要么是老数组哈希化得到的值加上老数组的长度。
也就是说老数组链表中的元素会被分配到这个两个位置的链表中,而当前位置链表长度为1,它只能分配到一个位置,不会发生覆盖情况。

2、链表元素不为1

else {
    // loHead表示放入当前j位置链表的链表头,loTail表示链表尾,
    // 因为Node是单向链表,所以要用链表尾变量,将键值对插入到链表尾
    Node<K,V> loHead = null, loTail = null;
    // loHead表示放入当前j+oldCap位置链表的链表头,loTail表示链表尾,
    Node<K,V> hiHead = null, hiTail = null;
    Node<K,V> next;
    do {
        next = e.next;
        // (e.hash & oldCap) == 0 表示对n+1位是0,那么它在新数组中的哈希化后的值和原来的一样
        // 将元素放入loHead链表中
        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循环遍历这个链表
    } while ((e = next) != null);
    // 将链表放到新数组对应位置。
    if (loTail != null) {
        loTail.next = null;
        newTab[j] = loHead;
    }
    if (hiTail != null) {
        hiTail.next = null;
        newTab[j + oldCap] = hiHead;
    }
}

那么这个链表就可能分为两个链表,放到本下标j位置,以及j+ oldCap下标位置。

所以我们要创建两个变量loHead和hiHead表示这两个链表,但是如果我们要保持链表的顺序不变,就还要创建两个变量loTail和hiTail表示链表尾,这样遍历链表的时候,就可以向新链表 链表尾插入元素,保证新链表的顺序和老链表一样。

具体步骤:

  • 通过while循环遍历链表。
  • 然后用(e.hash & oldCap) == 0 判断式,决定链表中的元素插入到那个链表中。
  • 将两个链表放入新数组对应位置。

resize()方法详解

final Node<K,V>[] resize() {
    // 使用oldTab表示原来的数组
    Node<K,V>[] oldTab = table;
    // oldCap表示老数组的长度, oldThr表示老的阈值
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    // newCap表示新数组的长度, newThr表示新的阈值
    int newCap, newThr = 0;
    // oldCap > 0表示table数组已经创建了
    if (oldCap > 0) {
        // 老数组长度已经最大容量了,那么就不能进行数组扩容,直接返回老数组
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 对老数组长度进行2倍扩容。
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0)
        // table数组还没有创建,threshold代表的是数组的长度,所以赋值给新数组的长度
        newCap = oldThr;
    else {
        // table数组还没有创建,且threshold为0,只有一种情况,那就是使用默认无参构造函数创建的Map集合
        // 所以使用默认的初始数组长度DEFAULT_INITIAL_CAPACITY(16)
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 这时可以直接计算出它的阈值, 即默认数组长度 16 * 默认负载因子 0.75f
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        // 因为新的数组长度newCap可能很大,所以要考虑超过最大容量上限问题,
        // 如果出现那样的情况,那么新的阈值就是int型最大值Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 将新阈值赋值给threshold
    threshold = newThr;
    // 根据新的数组长度创建新的数组newTab,并将它赋值给table
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 如果老数组不为空,那么就要将老数组中键值对元素迁移到新数组newTab中。
    if (oldTab != null) {

        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 这个是红黑树节点
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    // loHead表示放入当前j位置链表的链表头,loTail表示链表尾,
                    // 因为Node是单向链表,所以要用链表尾变量,将键值对插入到链表尾
                    Node<K,V> loHead = null, loTail = null;
                    // loHead表示放入当前j+oldCap位置链表的链表头,loTail表示链表尾,
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // (e.hash & oldCap) == 0 表示对n+1位是0,那么它在新数组中的哈希化后的值和原来的一样
                        // 将元素放入loHead链表中
                        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循环遍历这个链表
                    } 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;
}

这个方法返回扩容之后的新数组,主要可能进行那个步骤,创建符合长度的新数组,确定新的阈值threshold,将老数组中元素移到新数组中。

这个还分为三种情况:

  • 老数组存在,一般是将老数组进行两倍扩容,但是如果老数组长度已经是最大容量MAXIMUM_CAPACITY,不能再扩容了,那么就直接返回老数组。
  • 老数组不存在,那么阈值threshold就是新数组的长度,还有一种特殊情况,就是threshold也是0。那么它是HashMap空参构造函数创建的,特殊处理。
  • 将老数组元素移动到新数组,这个在再哈希过程详细说了。

源码解析

主要成员属性

// 默认初始容量 16,必须是2的幂数。 即只能是 16 , 32 , 64 等等
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 最大容量上限
static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 将链表转成红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;

// 用来储存链表的数组
transient Node<K,V>[] table;

// 缓存entrySet集合,避免每次调用entrySet()方法,都要重新生成Set集合
transient Set<Map.Entry<K,V>> entrySet;

// HashMap集合元素数量
transient int size;

// 记录集合修改次数,在多线程情况下,用于判断集合是否被修改
transient int modCount;

// HashMap集合阈值,集合元素数量超过这个值,那么就要扩充数组大小
int threshold;

// 负载因子
final float loadFactor;

静态内部类Node

static class Node<K,V> implements Map.Entry<K,V> {
    // 这个值是通过static final int hash(Object key)静态方法得到的,它和key的哈希值有关
    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;
    }

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

    // 将key和value的哈希值异或,得到一个新的哈希值。
    // 而且它保证的了相同key和value的Node,它们得到的哈希值是一样的。
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    // 替换键值对中的value值,并返回被替换的值
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    // key和value都相等的两个Node,它们是一样的。
    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;
    }
}

实现Map.Entry接口,HashMap储存的键值对元素就是Node类的实例对象,它有四个属性:

  • hash: 这个值是通过static final int hash(Object key)静态方法得到的,它和key的哈希值有关。通过这个值进行哈希化,得到对应下标值。
  • key:键值对中的key值
  • value:键值对中的value值
  • next:指向下个Node的引用,这样才能形成一个链表

构造参数

1、空参构造

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; defaulted
}

注意:空参构造只设置了loadFactor值,没有设置threshold值。

2、设置初始化大小和负载的构造函数

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

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;
    // tableSizeFor返回的是数组的长度大小,但为什么将它赋值给阈值threshold,
    // 那是因为创建数组不是在初始化的时候,所以threshold这里只是暂存数组长度,等创建完数组后,会改变这个值的
    this.threshold = tableSizeFor(initialCapacity);
}

通过tableSizeFor(initialCapacity)方法,保证数组长度是2的幂数,因为数组还没有创建,threshold表示数组长度。

3、通过Map集合创建的构造函数

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

调用putMapEntries方法,将m集合中数据存入HashMap集合中。

重要方法

添加键值对元素

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

// 向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;
    // table为null或者table数组长度是0,都要去创建新数组
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // [i = (n - 1) & hash]表示哈希化得到的下标值,如果为null,表示要创建一个新列表,存放到对应位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果这个条件相等,表示要插入的键值对元素key值在Map集合中已存在,那么就替换value值,e就表示已存在的键值对元素
        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 {
            // 遍历整个链表,binCount用来记录链表中键值对数量,用来判断是否将链表转成红黑树
            for (int binCount = 0; ; ++binCount) {
                // 为null,表示已经到了链表尾,就直接插入这个键值对
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 将链表转成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果发现key键一样,那么就直接跳出循环,执行value值覆盖操作
                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;
            // 当原来的oldValue为null,或者onlyIfAbsent的值为false,那么就进行覆盖
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 用于LinkedHashMap回调的,在HashMap中是空实现
            afterNodeAccess(e);
            // 直接返回,因为没有新添加值
            return oldValue;
        }
    }
    ++modCount;
    // 超过阈值,那么调用resize()进行数组扩容
    if (++size > threshold)
        resize();
    // 用于LinkedHashMap回调的,在HashMap中是空实现
    afterNodeInsertion(evict);
    return null;
}

向HashMap集合中添加元素是通过putVal方法实现的。

方法参数说明:hash、key、value创建键值对元素需要,onlyIfAbsent表示是否阻止覆盖键值对元素的value值,evict用于afterNodeInsertion方法,主要用在LinkedHashMap中。

putVal方法的主要流程:

  • table为null或者table数组长度是0,那么通过resize()方法创建新数组
  • 利用hash值通过(n - 1) & hash公式获取哈希化后的下标值,然后判断该位置是否有链表,如果没有,通过newNode方法创建新的键值对元素,存放到该位置
  • 如果链表不为null,那么就是查找链表中有相同的hash值和key值的键值对元素。
  • 查看表头的键值对元素是否符合条件。如果符合条件,就用e记录表头元素
  • 如果不符合,判断表头元素是否是红黑树节点元素,如果是,那么调用红黑树的putTreeVal方法存放键值对元素
  • 如果还不是,那么遍历整个链表,binCount用来记录链表中键值对数量,用来判断是否将链表转成红黑树。
  • e != null表示没有添加新的键值对元素,而是找到相同key的键值对元素,通过onlyIfAbsent值,来判断是否要进行value值的覆盖。
  • 如果e == null表示添加新的键值对元素,那么就要将size值自增,然后判断是否超过阈值threshold,如果是,那么通过resize()进行数组扩容。

添加一个Map集合

public void putAll(Map<? extends K, ? extends V> m) {
    putMapEntries(m, true);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    // 获取集合m键值对元素的数量
    int s = m.size();
    if (s > 0) {
        // 如果table为null,表示数组还没有创建,这个时候threshold表示的是数组长度,而不是阈值,
        // 所以这里进行了不同处理
        if (table == null) {
            // s表示要存储键值对元素的数量。所以除以负载因子就得到所需要的数组长度大小
            float ft = ((float)s / loadFactor) + 1.0F;
            // 防止超过数组最大长度
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            // 如果所需要数组的长度比原先设置的大,就要重新计算数组长度
            // 不直接将t赋值给threshold,是因为Map集合数组长度必须是2的幂数。
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 如果table已存在,这个时候threshold表示阈值,所以s大于threshold时,就要调用resize方法进行数组扩容
        else if (s > threshold)
            resize();
        // 遍历集合m中所有的键值对元素,然后调用putVal方法,将键值对存放到本集合中
        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);
        }
    }
}

通过putMapEntries方法添加集合中全部元素,方法流程:

  • 先获取m集合的键值对元素大小s
  • 判断table是否为null,如果为null,那么通过s来计算数组的长度,因为数组还没有创建,这时threshold就表示数组长度,而不是阈值
  • 如果数组table已经存在,那么就判断m集合元素大小是否超过阈值threshold,如果是,那么就要通过resize()方法进行数组扩容
  • 遍历集合m中所有的键值对元素,然后调用putVal方法,将键值对存放到本集合中

获取键值元素

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

// 通过hash和key值获取对应的键值对Node,hash的值是通过hash(key)方法得到的
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // tab表示哈希表,n表示哈希表数组长度,first表示对应下标链表头元素。
    // (n - 1) & hash得到哈希化后的下标值
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 通过hash,已经key的equals方法来检查是否是相同的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;
}

通过key值获取键值对元素,主要流程:

  • 通过hash(key)获取hash值
  • 通过(n - 1) & hash得到哈希化后的下标值,进而得到对应位置的链表,如果为null,表示没有找到。
  • 先判断表头元素的hash值和key值是否与参数中的相同,如果相同就返回表头元素。
  • 如果不同,那么判断下一个元素是不是红黑树节点,如果是,就通过红黑树获取对应的元素
  • 否则,就遍历链表寻找对应的键值对元素。

删除元素

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

// 删除HashMap集合中的元素
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;
    // tab表示哈希表,n表示哈希表数组长度,index表示哈希化下标值,p表示链表头元素
    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;
        // 寻找对应hash和key的元素Node
        // 1.先比较表头元素,如果相等就是它,如果不相等,就获取表头下一个元素
        // 2.判断元素e是不是红黑树节点,如果是那么通过红黑树的getTreeNode方法获取对应节点元素
        // 3.如果不是,就遍历链表,找到相同元素
        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);
            }
        }
        // 找到要删除的元素node。如果matchValue为true,那么还要比较元素node的value值与传入的value是否相同
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 如果node是红黑树节点,那么调用红黑树的removeTreeNode方法,删除节点
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            // node == p相等,表示元素node是表头元素,那么就要重新赋值表头
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

通过removeNode方法删除集合中元素。

方法中参数说明:hash、key用来查找要删除的键值对元素,value、matchValue用来判断找到的键值对元素要不要删除,movable用于红黑树删除节点时需要

方法流程:

  • 首先通过hash与key查找出对应的键值对元素,这部分代码与getNode方法几乎一样
  • 如果node不为null,且matchValue为false或者传入的value值与node元素的value相同,那么就要删除该元素
  • 如果node是红黑树节点,那么调用红黑树的removeTreeNode方法,删除节点
  • node == p相等,表示元素node是表头元素,那么就要重新赋值表头
  • node != p,那么p.next = node.next,将node节点从链表中移除
  • 最后将size自减

HashMap的迭代器

因为HashMap可以返回key值的Set集合,value值的Collection集合,以及键值对的Set集合,它们是以键值对集合为主。

HashIterator:迭代器抽象父类

abstract class HashIterator {
    Node<K,V> next;        // 下一个键值对元素
    Node<K,V> current;     // 当前键值对元素
    int expectedModCount;  // 用于检查fast-fail异常的
    int index;             // 用来记录哈希表下标位置

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        // 如果哈希表数组已经创建了,并且哈希表中有值,那么要寻找第一个元素
        if (t != null && size > 0) {
            // 从数组下标0开始寻找链表,并将链表头赋值给next
            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;
        // 将next赋值给e,用于返回
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        // 如果e.next为null,表示这个链表已经到结尾了,那么就要寻找下一个链表
        if ((next = (current = e).next) == null && (t = table) != null) {
            // 寻找下一个链表,并将链表头赋值给next
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        // 当前遍历到元素current
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        // 调用removeNode方法删除当前元素
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

注意:因为HashMap是通过哈希表储存的,那么就表示数组有的下标位置是空值,所以遍历哈希表,就要找到数组中所有的链表。而在HashIterator就通过很好的方式找到链表。如下:

do {} while (index < t.length && (next = t[index++]) == null);

当next不为null时,表示找到链表了,跳出循环。index++从0开始自增,表示会遍历完整个数组。

KeyIterator、ValueIterator和EntryIterator

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

KeySet类

public Set<K> keySet() {
    Set<K> ks = keySet;
     // 延迟创建keySet
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    // 返回KeyIterator迭代器
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    // 调用HashMap的containsKey方法
    public final boolean contains(Object o) { return containsKey(o); }
    // 调用HashMap的removeNode方法
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    // 返回Spliterator
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    // 遍历整个哈希表每个元素,调用action方法
    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,传入key值
                    action.accept(e.key);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

Values类

public Collection<V> values() {
    Collection<V> vs = values;
    // 延时创建values
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}    

final class Values extends AbstractCollection<V> {
    public final int size()                 { return size; }
    // 调用HashMap对应的clear方法
    public final void clear()               { HashMap.this.clear(); }
    // 返回ValueIterator迭代器
    public final Iterator<V> iterator()     { return new ValueIterator(); }
    // 调用HashMap的containsValue方法
    public final boolean contains(Object o) { return containsValue(o); }
    public final Spliterator<V> spliterator() {
        return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    // 遍历整个哈希表每个元素,调用action方法
    public final void forEach(Consumer<? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            // 遍历整个数组
            for (int i = 0; i < tab.length; ++i) {
                // 遍历链表
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    // 调用action,传入value值
                    action.accept(e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

EntrySet类

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    // 调用HashMap对应的clear方法
    public final void clear()               { HashMap.this.clear(); }
    // 返回EntryIterator迭代器
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    // 是否包含o对象
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        // 将o转成Map.Entry对象
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        // 得到key值
        Object key = e.getKey();
        // 通过的HashMap的getNode方法,获取对应的键值对元素candidate。
        Node<K,V> candidate = getNode(hash(key), key);
        // 返回是否包含
        return candidate != null && candidate.equals(e);
    }
    // 调用HashMap的removeNode方法移除键值对元素
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K,V>> spliterator() {
        return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

总结

HashMap是通过哈希表来储存元素的。

快速哈希化

HashMap是通过两个方面实现快速哈希化的问题:

  • 通过static final int tableSizeFor(int cap)方法,保证数组的长度是2的幂数(即2^n)。
  • 通过(n - 1) & hash快速得到哈希化的下标值。其中n表示数组的长度即2的幂数,hash并不是key的hashCode值,而是(h = key.hashCode()) ^ (h >>> 16)计算后的值(防止高位丢失问题)

数组扩容

当HashMap的元素容量超过阈值threshold,就要进行数组扩容了。最重要的操作就是将老数组中的值存放到新数组中。

因为数组长度是2的幂数2n,而每次扩容都是2倍即新数组长度是2(n+1)。
那么通过(length - 1) & hash获取哈希化的下标值,就只有两个情况,要么是原来的值,要么是原来的值加上老数组的长度。
那么老数组的链表就可能被分成两个链表,分别放在原来下标位置或者原来下标加上老数组的长度的位置。

主要方法

  • 添加或者替换元素: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
  • 通过key值获取键值对元素:final Node<K,V> getNode(int hash, Object key)
  • 删除键值对元素: final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)

 

posted @ 2022-02-07 09:23  残城碎梦  阅读(142)  评论(0编辑  收藏  举报