HashMap 浅度分析
Map结构是最常用的 数据结构之一,理解hashmap关键在于理解对于hash冲突的处理方式。
一、 默认常量
1、默认初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
题外话:要明白 如List结构中 capacity!=size
2、最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
取该值是由于int类型为5字节 最大为1<<31,而java基本数据类型为有符号数,所以最高位也就是符号位不应参与到本类中的各种方法运算中,诸如resize()方法,以及取新bucket的index等。
3、负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4、链表转树临界值
static final int TREEIFY_THRESHOLD = 8;
该值默认为8,与jdk1.8对于hash冲突的解决策略有关。
1.8之前使用链地址法解决hash冲突,而1.8采取了链地址+红黑树法解决hash冲突。
对于链式存储结构,其查找时间复杂度为O(N) 即对于长度为N的链表,平均时间复杂度为N/2,
而对于红黑树,其查找平均时间复杂度为O(log(n)).
对于长度为8的链表,其平均查找时间为8/2=4,
其转为树结构后,平均查找用时为log2(8)=3,
可见当长度在8附近时,算法时间复杂度差别并不大,而且生成树结构也要耗时,故默认的链表转树临界值为8.
5、树转链表临界值
static final int UNTREEIFY_THRESHOLD = 6;
原理与上相同,之所以设置差值应该是为了防止链表到树频繁转换,反而耗费额外时间。
6 最小数容量
static final int MIN_TREEIFY_CAPACITY = 64;
二、基础组件
1、节点
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;}
节点采用了Entry键值对结构,每个节点保存 键,值,hash值,下一节点的引用。
三、主要方法
1、(对给定初值)取表初始化容量
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;
}用于构造方法指定了容量初值时,由此方法计算 大于给定容量初值的 最小二次幂值。
对于Hashmap容量使用二次幂值,《Thinking in java》有一些解释:
①对于hash方法,java采用了本地方法实现
②理想情况下hash值应当分布的越平均越好,之前hashmap采用的容量值为质数,而后改为使用2的次幂,同时掩码运算比普通的除法快太多。
同时移位运算比乘法快的多
该方法执行流程为
①对给定值减一,为了防止对本身就是二次幂的给定值错误得取其二倍值作为容量。
②对给定值 逻辑右移一位 ,并对原值取或等,这样得到的就是对该给定值的最高1位的次高位设1的结果。
③将得到的值 与该值的逻辑右移2位 并对原值取或等,得到对给定值的最高1位的后三位都设1的结果。
以此类推,执行至2*16位,因为最大容量为1 << 30
这样得到的结果就是对给定值的 自最高1位至最低位均设1的结果,再将此结果加1,即得到2的整数次幂。该值也是大于或等于给定值的最小的二的整数次幂。
2、get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}该方法并非简单得取值,而要先判断给定key的节点是否存在。
之所以这样判断,是因为hashmap允许null作为键/值
3、put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}该方法被第一次调用时,会调用putVal()方法,生成bucket数组,即bucket并非Hashmap初始化时就存在的,而是当第一次put时才初始化出来
而putval()方法则主要目的是为了支持对存在hash冲突的key取值。
这就要求我们使用的key,必须重写hashcode()和equals()方法,这也是为何主要使用String和整数包装类作key的原因,一是因为其hashcode()和equals()方法已经重写,二是由于其不可变性,这对于多线程而言是非常重要的,这也是诸如Spring等架构使用无状态组件的原因。不可变就避免了多线程的安全问题。
4、resize()
初始化以及扩容时调用
这里主要是两部分(其实是我只看懂了这么些。。。)
①扩容
if((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
即每次都是两倍扩容,这跟前面原理一样
②重新分配bucket的index
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
新table的索引 index=该节点hash值e.hash & 新容量-1
这个与运算将对新最大容量以下的所有hash值取原值,对大于该容量的hash值截断
这样得到的新的节点,他在该bucket上的索引,①要么在原处,即原本其hash值的截断值的最高位的左一位为0;②要么最高位拓展出个新的最高位1,原最高位变为次高位,以此类推。
这种基于二进制的运算在Hashmap中作用发挥的淋漓尽致。

浙公网安备 33010602011771号