【图解JDK源码】HashMap的容量大小增长原理(JDK1.6/1.7/1.8)

1. 前言

HashMap的容量大小会根据其存储数据的数量多少而自动扩充,即当HashMap存储数据的数量到达一个阈值(threshold)时,再往里面增加数据,便可能会扩充HashMap的容量。

可能?

事实上,由于JDK版本的不同,其阈值(threshold)的默认大小也变得不同(主要是计算公式的改变),甚至连判断条件也变得不一样,所以如果说threshold = capacity * loadFactor(容量 * 负载因子)将不再绝对正确,甚至说超过阈值容量就会增长也不再绝对正确,下面就以JDK1.6、1.7、1.8中的源码说明。

注:本文无图,标题仅是为了与前一篇文字标题符合

2. JDK 1.6

JDK 1.6中HashMap构造函数源码如下(其中以Mark开头注释以及中文注释,非JDK源码中注释,下同):

public HashMap(int initialCapacity, float loadFactor) {
    // Mark A Begin
    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);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    // Mark A End

    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor); // 计算阈值,重点在这句代码
    table = new Entry[capacity];
    init();
}

其中,代码片段A(Mark A Begin - Mark A End,下同)处可以先忽略,主要的是

threshold = (int)(capacity * loadFactor);

这边是阈值的计算公式,其中capacity(容量) 的缺省值为16,loadFactor(负载因子)缺省值为0.75,那么

threshold = (int)(16 * 0.75) = 12

再来看addEntry函数(put(K, V)方法最后通过此函数插入数据,具体参见【图解JDK源码】HashMap的基本原理与它的线程安全性):

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)  // 判断是否扩充容量
        resize(2 * table.length);
}

可以看见,只要当前数量大于或等于阈值,便会扩充HashMap的容量为其当前容量的2倍。这是在JDK 1.6下的特性。

3. JDK 1.7

JDK1.7中HashMap构造函数源码如下:

public HashMap(int initialCapacity, float loadFactor) {
    // Mark A Begin
    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);
    // Mark A End

    this.loadFactor = loadFactor;
    threshold = initialCapacity; // 计算阈值,重点在这句代码
    init();
}

同样的,代码片段A可以先忽略,那么对么上面代码,可以看出,阈值的计算与JDK 1.6中完全不同,它与合约因子无关,而是直接使用了初始大小作为阈值的大小,但是这仅是针对第一次改变大小前,因为在resize函数(改变容量大小的函数,扩充容量便是调用此函数)中,有如下代码:

threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

也即是说,在改变一次大小后,threshold的值仍然跟负载因子相关,与JDK 1.6中的计算方式相差无几(未讨论容量到达最大值1,073,741,824 时的情况)。

而addEntry函数也与JDK 1.6中有所不同,其源码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) { // 判断语句发生了改变
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

从上面的代码可以看出,在JDK 1.6中,判断是否扩充大小是直接判断当前数量是否大于或等于阈值,而JDK 1.7中可以看出,其判断是否要扩充大小除了判断当前数量是否大于等于阈值,同时也必须保证当前数据要插入的桶不能为空(桶的详细可参见【图解JDK源码】HashMap的基本原理与它的线程安全性)。那么JDK 1.8中又是如何呢?

3. JDK 1.8

说明:JDK 1.8对于HashMap的实现,新增了红黑树的特点,所以其底层实现原理变得不一样,再此不讨论。

JDK 1.8中HashMap构造函数源码如下:

public HashMap(int initialCapacity, float loadFactor) {
    // Mark A Begin
    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);
    // Mark A End

    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity); // 计算阈值
}

这里使用到了tableSizeFor方法,其源码如下:

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

因为使用了位运算,所以这个方法可能不能明确的知道结果,但是只要知道不管输入什么值,它的最后结果都会是0,1,2,4,8,16,32,68… 这些数字中的一个就对了(其实是有规律的),对于以下输入值有:

tableSizeFor(16) = 16
tableSizeFor(32) = 32
tableSizeFor(48) = 64
tableSizeFor(64) = 64
tableSizeFor(80) = 128
tableSizeFor(96) = 128
tableSizeFor(112) = 128
tableSizeFor(128) = 128
tableSizeFor(144) = 256

也即是说,对于容量的初始值16来说,其初始阈值便是16,与JDK 1.7中初始阈值相同,而其resize函数中,threshold的计算源码如下:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor; // 负载因子在这里
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;

    // 代码太多,省略后面的代码
}

计算变得更复杂,因为其底层实现原理已经不仅仅是像之前的JDK中数组加链表那样简单,但是仍然可以看见其阈值的计算是与负载因子相关的。

而其判断是否要扩充的语句在putVal函数内(put方法会调用),其源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // 代码太多,省略
    ++modCount;
    if (++size > threshold) // 判断是否扩充语句
        resize();
    afterNodeInsertion(evict);
    return null;
}

可以看见,其判断是否达到阈值与JDK 1.6是相同的,而没有像JDK 1.7中那样判断桶是否不为空。

总结

JDK 1.6 当数量大于容量 * 负载因子即会扩充容量。

JDK 1.7 初次扩充为:当数量大于容量时扩充;第二次及以后为:当数量大于容量 * 负载因子时扩充。

JDK 1.8 初次扩充为:与负载因子无关;第二次及以后为:与负载因子有关。其详细计算过程需要具体详解。

注:以上均未考虑最大容量时的情况。

时间: 2024-11-10 15:53:40

【图解JDK源码】HashMap的容量大小增长原理(JDK1.6/1.7/1.8)的相关文章

【图解JDK源码】HashMap的基本原理与它的线程安全性

1. 前言 能用图说清楚的,就坚决不用代码.能用代码撸清楚的,就坚决不写解释(不是不写注释哦). 以下所有仅针对JDK 1.7及之前中的HashMap. 2. 数据结构 HashMap内部通过维护一个Entry<K, V>数组(变量为table),来实现其基本功能,而Entry<K, V>是HashMap的内部类,其主要作用便是存储键值对,其数据结构大致如下图所示. 从Entry的数据结构可以看出,多个Entry是可以形成一个单向链表的,HashMap中维护的Entry<K,

JDK源码-HashMap

1,Map:映射表数据结构,通过key-value完成映射.HashMap的子实现主要包括:HashMap.LinkedHashMap.TreeMap.WeakHashMap.ConcurrentHashMap.IdentityHashMap.以下总结摘录自<Thingking In Java> -1,HashMap:使用Map集合的默认选择.因为HashMap对速度进行了优化.HashMap是Map基于散列表的实现,并取代了Hashtable.插入和查询的效率相对固定.可以通过构造器设置容量

jdk源码——HashMap

JDK1.7 从源码上看,HashMap 实现了Map接口 cloneable接口,和序列化接口 public class HashMap<K,V>    extends AbstractMap<K,V>    implements Map<K,V>, Cloneable, Serializable{ HashMap的默认初始容量为16 static final int DEFAULT_INITIAL_CAPACITY = 16; HashMap最大容量为2^30 st

JDK源码--HashMap(之resize)

1.HashMap源码阅读目标了解具体的数据结构(hash及冲突链表.红黑树)和重要方法的具体实现(hashCode.equals.put.resize...) 2.重要方法 hashCode 与 equals都是在AbstractMap中定义的 hashCode是各元素hash的累加 h += iter.next().hashCode(); equals 1.是否是本身; 2.是否是Map实例; 3.size是否相等; 4.比较每个value 重点在于put.resize具体实现步骤: put

jdk源码hashMap的1.7与1.8的比较

1.8链表的定义基本上与1.7相同,但是类名改为Node,但是node实现了Map.Entry接口,实质是一样的 static class Node<K,V> implements Map.Entry<K,V> { 1.8的hash值的算法更加直观一点,就是key的hashcode与无符号右移16位的hashcode异或,然后返回.这是为了当length比较小的时候,也能保证考虑到高低Bit位都参与到Hash的计算中,同时不会有太大的开销. static final int has

由JDK源码学习HashMap

HashMap基于hash表的Map接口实现,它实现了Map接口中的所有操作.HashMap允许存储null键和null值.这是它与Hashtable的区别之一(另外一个区别是Hashtable是线程安全的).另外,HashMap中的键值对是无序的.下面,我们从HashMap的源代码来分析HashMap的实现,以下使用的是Jdk1.7.0_51. 一.HashMap的存储实现 HashMap底层采用的是数组和链表这两种数据结构.当我们把key-value对put到HashMap时,系统会根据ha

JDK源码笔记-java.util.HashMap

HashMap 的存储实现 当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例: Java代码 HashMap<String , Double> map = new HashMap<String , Double>(); map.put("语文" , 80.0); map.put("数学" , 89.0); map.put("英语" , 78.2); HashMap 采用一种所谓的&quo

jdk源码阅读-HashMap

前置阅读: jdk源码阅读-Map : http://www.cnblogs.com/ccode/p/4645683.html 在前置阅读的文章里,已经提到HashMap是基于Hash表实现的,所以在讲解HashMap之前 ,有必要提前了解下Hash的原理. 参考<算法导论><算法>

JDK源码学习系列08----HashMap

                                                          JDK源码学习系列08----HashMap 1.HashMap简介 HashMap 是一个散列表,它存储的内容是键值对(key-value)映射. HashMap 继承于AbstractMap,实现了Map.Cloneable.java.io.Serializable接口. HashMap 的实现不是同步的,这意味着它不是线程安全的.它的key.value都可以为null.此外,