前言
作为一名java程序猿,与java都是老伙计了,在运用方面,相信读者都很熟练,然对其原理,有不少读者就很少探究,笔者也看过不少文档和视频,但终究还认为绝知此事要躬行。
常量参数概览
相信大多读者都了解就hashmap的底层结构,笔者也简单的介绍了一下。hashmap是一个k,v对存储的数组加链表结构,hashmap的长度即为数组的长度。本文是基于jdk1.8而写的,关于1.7的底层与1.8的差异,笔者简单概述一下,1.8相较于1.7最大的差异,底层的链表超过长度为8的时候,会可能转换成红黑树。
// hashmap的默认长度16,1左移四位即为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// hashmap的最大长度,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// hashmap的默认负载因子,hashmap扩容时判断,
// 当前hashmap的数据长度达到了(总长度*负载因子),即进行扩容.
// 如hashmap总长度为16,即hashmap存储数据长度达到16*0.75=12的时候
// hashap将会自动进行扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转换树的长度阈值,即超过这个长度,链表将转换成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 链表扩容阈值,即链表长度超过6的时候,会考虑优先扩容而不是转换成红黑树
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转换成树的hashmap阈值,即hashmap.size()总长度超过64,才有机会将链表转换成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
构造方法概览
// 最常使用之一,默认无参构造,负载因子more的0.75,长度为初始化默认的16
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 最常使用之一,带整型参数构造,指定初始化长度
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);
// 参数长度是否超过hashmap的最大长度,2的30次方,超过即给定hashmap最大长度
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 检查负载因子的值是否有效
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 计算hashmap的下次扩容的阈值,即达到阈值开始进行扩容
this.threshold = tableSizeFor(initialCapacity);
}
关于tableSizeFor(initialCapacity)
简单来讲就是返回到大于等于initialCapacity的最小的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;
}
方法主要涉及到移位以及或操作。简要剖析一下,比如说n 的值为01xx xxxx.
首先进行无符号右移一位操作,高位补0.即001x xxxx。在进行位或运算
001x xxxx
01xx xxxx 或运算
011x xxxx,即n |= n >>> 1;
的值为011x xxxx。
同理,将n右移两位,0001 1xxx,进行或运算
0001 1xxx
011x xxxx 或运算
0111 1xxx, 即n |= n >>> 2;
的值为0111 1xxx。
继续,右移四位, 0000 0111,进行或运算
0000 0111
0111 1xxx 或运算
0111 1111 即n |= n >>> 4;
的值为0111 1111。
相信读者看出来,每进行一次操作,最高位的1hou的xx陆续变成1,最终结果就是让高位为1的后面所有位数的值变成1,最后在进行+1运算。算出最接近参数且大于或等于参数的二次幂。
至于为什么n = cap -1.读者不妨实际计算一下,如果n直接等于cap,假如cap的值等于8.即0000 1000
0000 0100 右移一位
0000 1000 或运算
0000 1100 结果。
继续操作
0000 0011 右移二位
0000 1100 或运算
0000 1111 结果
继续
0000 0000 右移四位
0000 1111 或运算
0000 1111 结果
接着
0000 0000 右移八位
0000 1111 或运算
0000 1111 结果
最后
0000 0000 右移16位
0000 1111 或运算
0000 1111 结果
最终结果进行+1运算即为0001 0000 即值为16,而我们的预期值为大于或等于参数的二次幂,而参数8本身就是2的三次方,所以与预期结果不符,进行减一操作运算后最终结果即为8.
关于为何只右移16位就停止的操作因为int类型只有32位,每一次右移以及或运算后都已经将32位数的值进行陆续变更为1.读者可观察之前的过程,
第一次n |= n >>> 1;
后有两个位数为1
第二次` n |= n >>> 2;`` 后有四个位数为1.
可以推断第三次就有8位,第四次就16位,第五次就32位。那是在极端状况下,是一个非常大的数。通常初始化的map也就是在几十到几百的长度。
接下来看最后一个构造方法
// 参数为map的构造方法,构造出的hashmap包含参数里map的值
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
// 将入参中的map的entry即成员(k,v)放入新的map
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
// 判断新的map空的刚初始化还是已经存在值了
if (table == null) { // pre-size
// 计算当前参数的扩容阈值,去比较默认的扩容阈值
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
// 计算下次扩容的阈值
threshold = tableSizeFor(t);
}
// 超过扩容的阈值判断
else if (s > threshold)
// 扩容
resize();
// 循环插入到新的map
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
这个方法在hashmap中被多次调用,不只是为hashmap的构造方法服务。
后话
在本文中笔者介绍了hashmap的常量参数以及构造方法,熟悉了tableSizeFor
的底层思想和计算原理。