HashMap源码肝了一天!put和resize方法保姆级分析看不懂算我输!

面试中HashMap几乎是必问的,问到hashmap必然离不开扩容机制,话不多说,肝就完了。
我们知道HashMap底层是采用数组+链表来实现的,jdk1.8后又引入了红黑树,本文针对JDK1.8后的HashMap进行分析。

先说明一下HashMap中几个重要的字段:
  • table:数组
  • size:元素的个数,即hashMap中存放了多少个元素,有别于capacity(容量)(容量是指数组的长度!)
  • threshold:扩容阈值,等于负载因子*容量。

还有几个默认的常量:
1.默认负载因子0.75
2.默认容量16
在构造HashMap时没有传入参数就会使用默认的值。

先介绍一下三个构造方法,需要注意的是:在调用构造方法时不会去执行resize(扩容)操作,在首次put元素时才会resize

1.构造方法

无参构造:负载因子默认为0.75
源码如下:

	/**
     * Constructs an empty {@code HashMap} with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

一参构造:指定初始容量大小,调用了另外一个两参构造方法,参数分别是容量大小和默认的负载因子0.75。
源码如下:

/**
     * Constructs an empty {@code HashMap} 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 {@code HashMap} 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);
        //容量大小大于最大容量,将指定为最大容量1<<30,注意:容量大小永远是2的整数幂
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //如果负载因子不是数值或者小于零,抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //负载因子为指定的负载因子
        this.loadFactor = loadFactor;
        //threshold为下一次触发扩容的阈值
        this.threshold = tableSizeFor(initialCapacity);
    }

其中tableSizeFor方法源码如下:
        /**
     * Returns a power of two size for th given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
这个方法是用来返回一个大于输入参数且最近的2的整数次幂的数,例如参数是10,则返回16。知道这个作用就行了。
2.Put方法

put方法调用了putVal方法,源码如下:

  /**
   * Associates the specified value with the specified key in this map.
   * If the map previously contained a mapping for the key, the old
   * value is replaced.
   *
   * @param key key with which the specified value is to be associated
   * @param value value to be associated with the specified key
   * @return the previous value associated with {@code key}, or
   *         {@code null} if there was no mapping for {@code key}.
   *         (A {@code null} return can also indicate that the map
   *         previously associated {@code null} with {@code key}.)
   */
  public V put(K key, V value) {
      //调用了putVal()方法,hash(key)是计算key的hash值,具体为key的hashcode(简称为h)与h无符号右移16位进行异或运算。
      //注:一个良好的hash计算方法应尽可能减少hash冲突,此hash方法通过无符号右移16使高16位也能参与运算,用来减少hash冲突
      return putVal(hash(key), key, value, false, true);
  }

putVal()方法流程

  1. 首先判断table是否为null,如果为null就进行resize。(首次put元素会执行resize);
  2. 如果table不为null,计算元素要存放位置的下标,如果该下标处没有元素,则将元素直接放到此位置。
  3. 否则说明此位置已经有元素了。分三种情况:
    ①判断此元素key值与要存放的key是否是同一对象或者相等,如果是则直接覆盖此元素的值并返回被覆盖的值。
    ②判断此元素是否是红黑树节点,如果是,调用红黑树的putTreeVal()方法存放元素。
    ③否则就是一个链表,对链表进行遍历。遍历过程中,比较每个节点的key是否与要放入元素的key值是否是同一对象或者相等,如果遇到相等,则跳出遍历过程并返回被覆盖的值;如果遍历完都没有发现key相等,则将元素放到链表末尾,然后判断是否需要进行树化操作。
  4. 更新size(上面的操作中,可能是新增元素,也可能是覆盖已有的元素,如果新增就++size,如果是覆盖不会执行第4步),如果size>threshold则进行resize。

源码及分析如下:

putVal()源码详解

3.resize方法

resize流程

  1. 如果是非第一次扩容(初始化扩容),则进行两倍扩容,然后将原来的元素重新散列到新数组中,并返回新数组。
  2. 如果是第一次扩容,如果没有参数,按照默认的容量大小16和负载因子0.75进行初始化,并返回新数组。如果指定了初始容量或负载因子就按照指定的数值初始化,并返回新数组。

源码及分析如下:

resize方法详解
再分析一下散列方法:

  • 在jdk1.7时,是通过对每一个元素重新计算hash值进行散列的。
  • 而在1.8则是:通过元素的hash值与原数组长度进行与运算,结果为0则代表在新数组中的下标为原下标,结果为1代表在新数组中的下标为原下标+原数组长度
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值