深入理解 HashMap put 方法(JDK 8逐行剖析)

前言

注意:我们今天所有的一切都是基于 JDK 8,JDK 8 的实现和 JDK 7 有重大区别。

前面我们分析了 hashCode 和 hash 算法的原理,其实都是为我们解析 HashMap 做铺垫,因为 HashMap 确实比较复杂(如果你每一行代码都看的话,每个位移都纠结的话),虽然总的来说,HashMap 不过是 Node 数组加 链表和红黑树。但是里面的细节确是无比的优雅和有趣。楼主为什么选择 put 方法来讲呢?因为从楼主看来,HashMap 的精髓就在 put 方法中。

HashMap 的解析从楼主来看,主要可以分为几个部分:
1. hash 算法(这个我们之前说过了,今天就不再赘述了)
2. 初始化数组。
3. 通过 hash 计算下标并检查 hash 是否冲突,也就是对应的下标是否已存在元素。
4. 通过判断是否含有元素,决定是否创建还是追加链表或树。
5. 判断已有元素的类型,决定是追加树还是追加链表。
6. 判断是否超过阀值,如果超过,则重新散列数组。
7. Java 8 重新散列时是如何优化的。

开始吧!!!

1. 初始化数组

首先我们来一个测试例子:

  public static void main(String[] args) {
    HashMap<String, Integer> hashMap = new HashMap<>(2);
    hashMap.put("one", 1);
    Integer one = hashMap.get("one");
    System.out.println(one);
  }
}

一个简单的不能再简单的使用 HashMap 的例子,其中包含了对于 HashMap 来说关键的 3 个步骤,初始化,put 元素,get 元素。

由于我们预计会放入一个元素,出于性能考虑,我们将容量设置为 2,既保证了性能,也节约了空间(置于为什么,我们在之前的文章中讲过)。

那么我们就看看 new 操作的时候做了些什么事情:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

//=================================================================================
    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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 的两个构造方法,其中,我们设置了初始容量为 2, 而默认的加载因子我们之前说过:0.75,当然也可以自己设置,但 0.75 是最均衡的设置,没有特殊要求不要修改该值,加载因子过小,理论上能减少 hash 冲突,加载因子过大可以节约空间,减少 HashMap 中最耗性能的操作:reHash。

从代码中我可以看到,如果我们设置的初始化容量小于0,将会抛出异常,如果加载因子小于0也会抛出异常。同时,如果初始容量大于最大容量,则重新设置为最大容量。

我们开最后两行代码,首先,对负载因子进行赋值,这个没什么可说的。
牛逼的是下面一行代码:this.threshold = tableSizeFor(initialCapacity); 可以看的出来这个动作是计算阀值,上面是阀值呢?阀值就是,如果容器中的元素大于阀值了,就需要进行扩容,那么这里的这行代码,就是根据初始容量进行阀值的计算。

我们进入到该方法查看:

    /**
     * Returns a power of two size for the given target capacity.
     */
    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的幂次方(我们通过之前的分析,应该直到初始容量如果不是2的幂次方会有多么不好的结果)。通过位移运算和或运算,最后得到一定是2的幂次方,并且是那个离那个数最近的数字,我们仔细看看该方法:

| : 或运算,第一个操作数的的第n位于第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0

首先,将容量减1,我们后面就知道了。然后将该数字无符号右移1,2,4,8,16,总共移了32位,刚好是一个int类型的长度。在移动的过程中,还对该数字进行或运算,为了方便查看,楼主写一下2进制的运算过程,假如我输入的是10,明显不是2的幂次方。我们看看会怎么样:

10 = 1010;
n = 9;

1001 == 9;

1001 >>> 1 = 0100;
1001 或 0100 = 1101;

1101 >>> 2 = 0011;
110 或 0011 = 1111;

1111 >>> 4 = 0000;
1111 或 0000 = 1111;

1111 >>> 8 = 0000;
1111 或 0000 = 1111;

1111 >>> 16 = 0000;
1111 或 0000 = 1111;

最后,1111 也就是 15 ,15 + 1 = 16,刚好就是距离10 最近的并且没有变小的2的幂次方数。可以说这个算法非常的牛逼。楼主五体投地。

但是如果是 16 呢,并且没有不减一,我们看看什么结果:

16 = 10000;

10000 >>> 1 = 01000;
10000 或 01000 = 11000;

11000 >>> 2 = 00110;
11000 或 00110 = 11110;

11110 >>> 4 = 00001;
11110 或 00001 = 11111;

11111 >>> 8 = 00000;
11111 或 00000 = 11111;

1111

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值