目录
先看HashMap的类之间的关系图,全局了解它的位置。
介绍hashmap类中的几个关键属性字段(注意看注释,加以思考)
/**
* The default initial capacity - MUST be a power of two.
*/
//默认初始容量 16 Capacity指桶的数量(数组中的容量,非整体k v个数)
//容量必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子,当size值大于capacity*loadfactor时进行扩容
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
// bin指的是每一个KV
//树化容量:当一个桶中的KV个数超过(>)8的时候,则use a tree rather than list for a bin
static final int TREEIFY_THRESHOLD = 8;
//由树还原成链表的容量,为什么是6 (和8??需要思考一下)
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
// 容量(数组桶个数)>64的时候,才允许将链表变成红黑树
//否则:直接扩容,而非树化
//为了避免扩容,树化方式的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
Q:为什么容量必须是2的幂?
在查询一个key对应的位置index时,源码中是这样计算的:i = (n - 1) & hash n是数组的长度。因为要避免碰撞,所以在取值,元素的位置时要尽量分散不重复,所以这个i(也就是index)的取值规则很重要。hash值是Object类生成的一个hash值(int类型),本来就是随机的,所以与前面的(n-1)按位与(&)的话 ,只有2的幂-1的情况下,化成二进制都是1111111...,故再与hash按位与,则取决于hash的后几位,便均匀的分布在了0到n之间了。
举例:加入数组容量是n=16(默认值),那么15 & (随机n),则出现的值,一定是在0到15之间。则平均分布到了16个桶中,避免了冲突碰撞。
补充:& 是代表按位与,都是1才是1,否则为0
Q:为什么默认初始容量 16 ?
避免碰撞,取折中值。因为是8或者4的话很容易导致map扩容影响性能,如果分配的太大的话又会浪费资源,所以就使用16作为初始大小
Q:为什么树化容量是8,还原链表容量是6?
首先明白,树化是由链表转化成红黑树,因为链表太长,查找效率低,才会转化成红黑树,链表的查询时间复杂度 n/2,计算:
bin(kV值)长度为6:链表时间复杂度 6/2 =3 。
若变成红黑树,则时间复杂度 log(6)=2.58 (时间复杂度的计算,log默认是2为底)
则链表为6的时候,链表更好。
bin长度为7 时:链表时间复杂度:7/2=3.5
红黑树:log7=2.8
bin长度为8时: 链表时间复杂度 8/2=4
红黑树:log8=3 (更优!)
为什么不是7呢?这里应该是把7当成一个过度的点,不然元素增减频繁,转化也频繁,会耗费性能。
Q:为什么容量>64才能数化,并且防止冲突要至少大>4 * TREEIFY_THRESHOLD?
这个值的含义是,如果数组(table)长度小于这个值的话,则没有必要进行结构转化(链表变树)反而扩容方式更好。
当一个数组位置碰撞集中了很多个键值对,因为这些对象的 key的hash 值和(length-1) 相与 (&)得到的结果(index) 相同,但并不是hash相同的概率高,(可能仅仅是因为length太小了,所以会一直冲突碰撞),所以这种情况,扩容就好了,冲突会小一些),没有必要去转化成红黑树。
介绍HashMap的几种构造函数
1、传参 初始容量和负载因子
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;
this.threshold = tableSizeFor(initialCapacity);
}
主要方法:tableSizeFor(initialCapcity) 如何将传入的initialCapcity变成2的幂呢?文章后面会有讲解。
2、传参初始容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
其中this方法调用的是第一种构造函数,默认负载因子是static final float DEFAULT_LOAD_FACTOR = 0.75f;
3、无参构造
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
同样是默认负载因子,0.75。其他的都是默认值。
这里注意,在第一次使用这个map的时候,才会把整个数据结构(数组)初始化出来--first use
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node[] table;
小补充:transient的作用是,该字段(仅可修饰字段)在序列化的时候,不会被传递,参考作用:节省空间,无需传递。
4、传入Map
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
此时这个新构造的map中会把参数中的map初始化进去。
看下我们如何使用hashMap,刚才说table只有在第一次使用的时候,才会初始化整个map的数据结构,那么我们来看下源码,如何初始化一个hashmap。
PUT方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
其中hash方法如下
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashCode()表示的是JVM虚拟机为这个Object对象分配的一个int类型的数值。为什么要(h = key.hashCode()) ^ (h >>> 16)呢?参考 此文章,简而言之,让hash更均匀。
其中 putVal方法如下
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//如果是第一次使用,则 初始化table,主要使用 resize() 方法来初始化
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 如果此节点已转化成树的话,就按照树结构去填充
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果map中的数据大于 threshold=capacity(桶数量:数组数量)*loadFactor
//则进行扩容操作
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
Q:tableSizeFor是如何把用户传入的初始值,转化成最近(大)的一个2的幂的数?
先来看下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;
}
讲解这段算法之前,先介绍几个运算符号:
& | 按位与运算符 | |
| | 按位或运算 | 只要有一个为1,其值为1 |
^ | 异或运算符 | 两个相应位为“异”(值不同),则该位结果为1,否则为0 |
~ | 取反运算符 | 对一个二进制数按位取反,即将0变1,1变0 |
<< | 左移运算符 | 将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0) |
>> | 右移运算符 | 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃 |
>>> | 无符号右移运算符 | 右移 expression2 指定的位数。右移后左边空出的位用零来填充。移出右边的位被丢弃 |
查看上述n |=n>>>1 可以类比n+=1的计算方式来计算。
为什么是 n>>>1 .....n>>>16呢?
首先,1“n |=n>>>1”的意思是对一个二进制数依次向右移位,然后与原值取或。为了得到比传进来的参数(cap) 大的最近一个2的幂(2的幂的表现形式都是:1000......) ,所以,我们最后得到的n一定是111111....,最后再+1便得到了10000...这种2的幂的数。
接下来就要各位一定动手写一写,一下就明白了。举个例子:
假如我们传进来一个参数为17的容量cap,那么经过tableSizeFor算法计算的话,步骤如下:
细心的同学看到这里,应该就能明白了,为什么tableSizeFor(int cap),要先-1,再做位或运算。因为,如果参数传入的是16,也就是本来就是2的幂,那么,经过运算之后,会变成32,更大的数字。
另外:容量是一个int类型的,int占4字节,一共32位。并且最大容量是1>>>30。(所以,n |=n>>>1一直到16,能把30位的最大的数字都覆盖上)
Q: resize()的作用有两个 ①初始化 ②扩容
第一次使用的时候,才会初始化整个table,也是用的resize()方法,然后等容量大于threshold的时候,就会再次扩容,容量变成原来的2倍。
【链表/红黑树】
与1.7的主要区别是
TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
HashEntry 修改为 Node。
变树的条件:①链表上的长度>8 & ②容量>64 两者缺一不可。上文中已经讲过原因。那为什么是红黑树呢?下面来讲一下各种树。
如果不了解树结构的,可以参考:此文章
推荐文章:红黑树特性 红黑树对比AVL
红黑树和AVL树都是最常用的平衡二叉搜索树,他们支持插入,删除和查找。logN
红黑树 | AVL |
适合插入,删除密集场景 | 平衡条件更加严格,适合查找 |
更通用(添加,删除,查找) | 查找速度快,但添加删除慢 |
旋转难实现和调试 |