“ 本文将主要介绍New HashMap()做了些什么以及Node<K,V>数组是怎么进行初始化的。”
01
—
默认属性
/**
* 数组 Node<K,V>[] table
* 默认初始容量 2^4 = 16
* 必须是2的多少次幂等
* 是数组的长度
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 数组 Node<K,V>[] table
* 最大容量 2^30=1073741824
* 可能用户使用带有容量的构造方法
* 初始化map的时候会写一个很大的值
* 比较后最大也就是 2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 数组 Node<K,V>[] table
* 默认使用率 0.75f
* 如果用户没有指定则采用该值
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 数组 Node<K,V>[] table
* 桶中存储元素优先采用单向链表
* 当链表节点数大于等于该值时
* 可能会触发单向链表转双向链表构造红黑树
* 还有条件是table表容量
* 必须大于等于
* MIN_TREEIFY_CAPACITY
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当桶中节点数小于等于该值时
* 触发双向链表退化成单向链表
* 不再构造红黑树
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当Node<K,V>[] table的容量
* 即数组长度 大于等于该值
* 且桶中节点数大于等于TREEIFY_THRESHOLD
* 触发桶中单向链表转双向链表构造红黑树
*/
static final int MIN_TREEIFY_CAPACITY = 64;
关于HashMap默认属性我们总结如下:
02
—
成员属性
/**
* 数组Node<K,V>[] table
* map的key hash后对数组长度取余的值
* 通过数组下标可以快速定位数据存储在数组的哪个位置上
* 也就是数据在哪个桶中
*/
transient Node<K,V>[] table;
/**
* 遍历HashMap入口 称为视图
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* map的长度 即有多少个key-value
*/
transient int size;
/**
* map结构修改次数
*/
transient int modCount;
/**
* map容量触发扩容的阈值
* 如果当前map含有的节点数超过该值
* 就会调用resize()方法进行扩容
*/
int threshold;
/**
* table数组的使用率
* 用于计算触发下一次触发扩容的阈值
*/
final float loadFactor;
03
—
构造方法
/**
* 无参构造方法 我们也最常用
* 采用默认数组长度 16
* 默认使用率75%
* 而我们只看到了使用率进行了赋值
* 那数组长度呢 在resize()中
* 我们下一小节初始化一起说
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/**
* 设置初始容量构造方法
* 这里调用的是另外一个构造方法
* 传入的参数是客户定义的容量
* 和默认的使用率75%
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 设置初始容量和使用率的构造方法
*/
public HashMap(int initialCapacity, float loadFactor) {
//初始容量必须大于0
if (initialCapacity < 0){
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
}
//如果容量大于最大值 着赋值容量为最大值
if (initialCapacity > MAXIMUM_CAPACITY){
initialCapacity = MAXIMUM_CAPACITY;
}
//使用率不能小于等于0
if (loadFactor <= 0 || Float.isNaN(loadFactor)){
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
}
//赋值使用率
this.loadFactor = loadFactor;
//赋值扩容阈值 而且是经过计算的 再看下计算了什么
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Returns a power of two size for the given target capacity.
* 看着这些不怎么使用的计算方式
* 复制测试了下 发现其实就是为了变为2的幂次方
* 比如传入10 返回16 传入16返回16 传入18返回32
*/
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;
}
/**
* 直接传入Map进行构造
* 首先赋值为默认使用率
* 再把整个Map遍历放入
*/
public HashMap(Map<? extends K, ? extends V> m) {
//赋值使用率为75%
this.loadFactor = DEFAULT_LOAD_FACTOR;
//放入Map
putMapEntries(m, false);
}
/**
* 装入Map
* evict参数留给了子类去扩展使用
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取装入Map的长度
int s = m.size();
//长度大于0才执行操作 否则是个空的 不用执行遍历赋值操作
if (s > 0) {
//如果数组为空
if (table == null) {
//采用默认数组使用率计算数组的容量
//+1应该是为了保证下面下取整不够了
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();
}
//循环遍历 插入值
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);
}
}
}
构造方法总结如下:
疑问如下:
好,带着疑问我们接着往下看。
04
—
初始化
计算HashMap对象已经有了,那我们肯定是要向里面放数据。我们看下put方法。
/**
* put操作直接调用了 putVal
* 计算了要放入key的hash值
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 计算key的hash值
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 实际插入值方法
* 先判断数组是否为空 为空则调用resize()
* 再次看到了resize()
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断Node数组是否为空 为空则进行初始化
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}
...
}
关于hash(key)的实现,通过百度了解,主要是为了有更好的分散,减少hash碰撞。想了解可自行百度了解。接下来让我们看看resize()。
/**
* 实现数组的创建和扩容
*/
final Node<K,V>[] resize() {
//赋值操作 oldTab为原数组
Node<K,V>[] oldTab = table;
//原数组长度 为空则为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//赋值原扩容阈值
int oldThr = threshold;
//定义新数组长度和扩容阀值
int newCap, newThr = 0;
//如果原数组长度大于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
}
//如果原扩容阀值大于0 则赋值新数组长度为扩容阀值
//数组初始化 对应的是含有容量的构造方法
//这也是为什么要在构造方法中计算阀值是2的幂次方
//疑问b解决了
}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;
@SuppressWarnings({"unchecked"})
//终于看到我们的Node数组实例化了
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//赋值给成员变量
table = newTab;
//其他操作暂时省略
...
return newTab;
}
疑问解决:
初始化总结:
HashMap的初始化就学习到这里,想继续跟随我学习了解HashMap的其他源码,就给加个关注、点个在看。
记得关注哦,关注木左不迷路,木左带你上高速!