今天在提交代码的时候。提交了一行
HashMap<String, Object> pageMap = new HashMap<>(3);
特傻逼的代码。被老大指出太业余。虽然自己也知道hashmap的容量要设置成2的幂次,但是实际使用起来却没有丝毫的注意。
说明还是对Hashmap的理解不够透彻,故决定整理一下Hashmap的笔记。
Hashmap具有数组和链表的优点。
使用hash算法加快访问速度,使用散列表解决碰撞冲突的问题,
数组的每个元素是单链表的头结点,链表是用来解决冲突。
hashmap是线程不安全的
HashMap中put方法,put(K key, V value)
如果key的哈希码不存在,加入元素。
否则,获取key的哈希码,计算出他的bucketIndex,
去除bucketIndex上的元素,循环单链表。有值得话把旧值替换了,没有值的话加入链表。
Hashmap的两个重要参数:初始容量和加载因子。
初始容量是hash数组的长度,
当前加载因子=当前hash数组元素/hash数组长度。
最大加载因子为最大能容纳的数组元素个数。
哈希表的容量一定是2的整数次幂。
(为不相等的对象生成不同整数结果可以提高哈希表的性能)
初始容量是16,初始加载因子是0.75。容量是哈希表中桶的数量,初始容量是哈希表在创建时的数量,加载因子是其容量在其自动增加前可以达到的最大的尺度,当哈希表中的数量超过最大加载因子和容量的乘积,就要对hashmap进行扩容。
看hashmap的构造方法,在初始化容量的时候,不管初始化容量是多少,都返回最近的不小于初始化容量的2的整数次幂
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);
}
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;
}
先让cap-1是为了找到目标值大于或等于原值,
再进行五个位移操作,是为了使cap的二进制从全部变成1(因为int为32位,所以最后位移16位就能实现)
对n |= n >>>?举例解释:
n=1000000
n|=n>>>1;n=n|(n>>>1)=1000000|0100000=1100000
n|=n>>>2;n=n|(n>>>2)=1100000|0011000=1111000
。。。。
所以不管初始值设置了多少。最后都会变成距离设置的值最近的不小于它的二次幂。
现在分析假如源码不做这个转化成二次幂的操作。
看put源码
if ((p = tab[i = (n - 1) & hash]) == null)
在put的时候先进行(n-1)&hash的判断。n是设置的hashma初始值,hash是put值得key生成的hashcode
假如n不是二次幂。那么n-1的二进制肯定有一位不为1
用15举例,15-1=14,二进制是1110,那么任何数和1110进行&操作,第一位永远不可能为1,所以第一位为1的空间全部都浪费了。
再回到老大指责我的问题,我的设置其实不会有什么影响,但是一眼就能看出,太不专业了。
补充Java8 hashmap的put原理,源码就不贴了,大致记录一下自己理解的流程。
先(n-1)&hash判断这个位置有没有值,没有的话直接插入,
有的话判断这个位置的类型是不是树结构,是的话直接添加树,
不是的话就加入链表,与1.7不同的是,1.7是头插法,后来的留在数组,1.8是先来的在数组,后来的链在尾上。
最后如果链表长度为7,再添加一个时候链表就要变成红黑树了。
优势在于,1.7的链表查询时间复杂度为o(n),1.8的红黑树查询时间复杂度是o(logn)
再补充一下HashMap 的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n代表数组长度)。这也就解释了 HashMap 的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。