HashMap的构造函数们
先认识几个默认常量:
/**
* The load factor used when none specified in constructor.
* 默认的负载因子0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The default initial capacity - MUST be a power of two.
* 默认初始容量 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
* ,最大容量,容量取值范围[2,230]
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
构造函数有四个,下面分别简单说一下:
// 默认构造函数,全都采用默认值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 指定“容量大小”的构造函数
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会返回一个大于initialCapacity的最小二次幂
// threshold临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
- 从上面这段代码我们可以看出,在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组。
扩容机制
- 条件
HashMap的扩容是根据threshold变量来判断的,扩容操作一般发生在。
when size>threshold
size是实际存放数据的量(两个hash值相同的数据,会计数两次)
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容 int threshold;
- 时间
一般发生在往表中put数据的时候。 - 扩容方法 resize()
- 扩容的过程其实是找一块新的连续的内存地址,然后将之前的值复制过去,重新进行hash映射。
为什么大小永远是2的幂次
首先要知道,key到index的映射是怎么实现的:
hash=key.hashCode();
index = (length - 1) & hash;
//length为数组的长度
从key获取到key存放的数组的下标index,是先取key的hash值,然后将hash值和数组长度减一进行了与运算(而不是传统的取模运算%)
这是为了提高映射的效率。每次存、取、扩容操作都会涉及大量的key映射inde运算,特别是每次扩容,曾经存好得到数据又要全部重新计算index。
好了前面说完了现在来说为什么数组大小要是二次幂。
“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。
位运算和取模运算的运算效率对比 简书地址
链表和红黑树的转换
这里又涉及了一些预定义好的常量
/**
* 将链表转化二叉树的阈值 8
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 将二叉树转化回链表的阈值 6
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 将链表转化成二叉树时,最小容量值 64(注意:不是 map 中的元素个数)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
为什么转换阈值设为8呢?
HashMap节点分布遵循泊松分布,按照泊松分布的计算公式计算出了链表中元素个数和冲突概率的对照表,可以看到链表中元素个数为8时的概率已经非常小。
下一章开始详细学习一下put和get方法