java api系列目录
简述
首先看一下官方文档是怎么介绍HashMap的:
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.
简单来说,就是:
- 基于Map接口来实现,支持所有的map操作
- Key和Value都支持null值
- 不保证有序,无论是插入顺序,还是Key的排序等
- 不保证固定顺序,随着容量的增大或减少,顺序会发生变化
举个例子就很清楚了:
1 | Map map = new HashMap(); |
通过IDEA断点查看里面的存储结构是这样的,可以看到插入顺序是打乱了的:
影响映射分布的两个因子
第一个是hash表的capacity(容量):
The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created.
第二个是load factor(负载因子):
The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
当hash表存储的数据量超过capacity 乘以 load factor时,hash表会扩容,容量会翻倍,部分数据会重新映射到扩容后的地方。所以初始容量和负载因子是影响HashMap性能及分布的两个重要因素。
1 | static final float DEFAULT_LOAD_FACTOR = 0.75f; |
HashMap默认的负载因子是0.75,这个是比较折中的数字,平衡空间和时间的开销。负载因子如果太大,会造成hash冲突严重,影响查询性能;如果太小,会容易触及扩容,浪费空间。
有效预估HashMap存储的实际数据量,来设定初始容量,可以有效避免经常扩容导致的性能开销。
put方法详解
1 | public V put(K key, V value) { |
总结起来就是:
- 通过hash计算偏移量获得节点位置
- 如果位置没有旧节点,则新建节点存放
- 如果旧节点的key等于新节点的key,则用新的value覆盖旧的value
- 如果旧节点是树节点,则以树的形式添加新节点
- 如果是一个链表,则以链表的信息添加新的节点,重复则覆盖,不然的放置尾端;链表过长,则转换成树
- 如果table大小发生改变,并且触发扩容,则扩容
hash函数详解
看完put方法,大致对HashMap整个结构有了个大概的了解,那么其中一个比较重要的步骤,通过hash值计算table偏移量,这个决定了新旧值碰撞概率,以及扩容时,节点移动时的效率。
下面是API实现的hash方法:
1 | /** |
为啥要将高16位和低16位做异或操作,其实再put方法里面,我们看到计算偏移量的计算是这样的:(n - 1) & hash
,n的大小一般不会很大,超过16位(65535);所以高低位异或的目的,是让n小时,高16位也能参与hash计算,增加散列的随机性。
get方法详解
有了put方法里面讲解的节点存储结构,及hash的计算,那么就很容易理解get方法了
1 | public V get(Object key) { |
resize方法详解
有了前面的铺垫,那么怎么设计扩容,才能使节点移动最少呢?
官方是这么说的:
1 | Initializes or doubles table size. If null, allocates in |
在扩容时,将table的大小乘以2,为什么是乘以2而不是3?
在计算偏移量:(n - 1) & hash
时,n是比较关键的参数,如果n一开始是16,那么n-1的二进制是:1111,那么偏移量计算就是与hash的低4位&操作;如果n = n * 2 为32,n-1的二进制是:11111,那么偏移量计算就是与hash的低5位&操作。
这样的话,原来的节点将发生什么变化呢?
如果原来存在这样两个节点,hash值分别是A:101011和B:111011,在n=16时,这两个节点是发生碰撞的(以链表或树的形式共存),偏移量都是1011;扩容后,A的偏移量是01011即1011保持不变,B的偏移量是11011发生节点转移。
看到这里,就明白了这样的扩容设计,保持了一半左右的节点不需要移动,而大部分碰撞节点,将迁移到新的位置,使存储和查询更加高效。
1 | final Node<K,V>[] resize() { |
整篇下来,基本就可以了解到HashMap的实现原理了,介于篇幅有限,文章中埋了个坑,树节点没怎么讲,想要了解更多,就持续关注 java api系列 ,并点赞留言告知。