这里是修真院后端小课堂,每篇分享文从
本篇分享的是:【HashMap 】
(1)背景介绍:
不讲HashMap的使用方法,看一看底层的源码是什么?
思考:HashMap使用key,·value进行存储,使用的数据结构是什么?
我们知道数组和链表两种数据结构
数组:
优点:查询速度快
缺点:增加和删除慢
链表:
优点:增加和删除快
缺点:查询速度慢
那我们可不可以将两者的优点结合一下,达到查询、增加、删除效率都非常快呢?
所以我们猜测一下,HashMap能否源码底层数据结构是采用的链表+数组的形式呢?
(2)知识剖析:
1.HashMap数据底层具体存储的是什么?
Java是一门面向对象开发的语言,可以把所有东西可以看做是对象。
通过查看源码可知:map里面的key和value都保存在了这个Node对象里面。
class Node<K,V>{
private K key; //用来定位数组索引位置
private V value;
private Node<K,V> next; //链表的下一个node
}
2.数组怎么表示?
transient Node< K,V>[] table;
每个列表被称为桶,即哈希桶数组,是一个Node的数组。
HashMap使用哈希表进行存储。怎样得到表中对象的索引位置?
key.hashCode( )----->hashCode----->Hash算法的高位运算和取模运算----->得到存储位置
有时两个key会定位到相同的位置,就发生了Hash碰撞。Hash算法计算结果越分散均匀,Hash碰撞的概率就越小,map的存取效率就会越高。
如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞。
3.数组要不要有一个大小呢?
学过数组都知道,要给数组一个初始化大小,那么HashMap有没有大小呢?
Defalutsize = … 初始大小???
Maxmumsize = … 上限大小???
肯定是要有的。
4.数组初始大小是多少呢?
从源码可以看到
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
初始大小是16
如果初始大小不够用,或者达到某个值,是不是要进行数组大小的扩容?
final Node<K,V>[] resize()
5.扩容是不是要有一个依据?
int threshold = 16; Node[] table的初始化长度
transient int size; HashMap中实际存在的键值对数量
size > threshold * 小于1的小数
实际使用的size肯定不可能达到16才进行扩容,那样会损失性能,一定要达到一个点就进行扩容,这个点是0.25 0.5 0.75哪一个呢?
源码中定义了一个扩容因子
static final float DEFAULT_LOAD_FACTOR = 0.75f
size = threshold * Load factor ----->16 * 0.75 =12
所以在12的时候就会进行扩容。
为什么要选择0.75?
默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
6.链表的是不是需要一个长度限制呢?
负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。链表达到一个限制值后,是否需要进行一个改变?
在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。
当链表长度太长时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能
源码中定义,当链表超过8时,就转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
7.Node节点到底存在哪里?
存在哪里要有一个位置,这个位置就是通过计算得来的。
利用这个key进行一个计算,计算出这个Node应该存在哪里。
想知道key,value存放在HashMap结构中的位置,使用hash函数,源码的实现(方法一+方法二):
方法一:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
return h & (leng
(3)常见问题:
HashMap线程安全吗?
(4)解决方案:
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全在多线程使用场景中,应该尽量避免使用线程不安全的HashMap,而使用线程安全ConcurrentHashMap。
(5)编码实战:
比如我的一个key的哈希值
h=hashCode():
11111111111111111111000011101010
h>>>16:
00000000000000001111111111111111
hash=h^(h>>>16):
11111111111111110000111100010101
(n-1)&hash:
11111111111111110000111100010101
最后取后四位0101转换成十进制就是5
(6)拓展思考:
JDK1.8与JDK1.7的性能对比?
JDK1.8引入红黑树大程度优化了HashMap的性能。
(7)参考文献:
Java 8系列之重新认识HashMap——美团技术团队
(8)更多讨论:
Q1.Map集合还有哪些实现类?
A1:LinkedHashMap、Hashtable和treeMap
Q2.HashMap为什么要保证hashmap的容量为2的幂次方?
A2:HashMap容量为2的幂次方,是为了减小哈希冲突,因为如果存在哈希冲突的时候,存储entry的时候,会出现一个位置上存在多个entry的情况,会造成读写速度的降低;使用2的幂次方的容量,存储的时候,会根据key的哈希值跟hashmap的(容量-1)进行位运算,这样的运算,当(容量-1)的二进制数全为1的时候,存储位置的计算结果是哈希冲突最小的。
Q3.HashMap优缺点?
A3:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
(9)鸣谢: