智者不为愚者谋,勇者不为怯者死。这篇文章主要讲述Java 集合系列07--- HashMap详细介绍(源码解析)----新相关的知识,希望能为你提供帮助。
前言【Java 集合系列07--- HashMap详细介绍(源码解析)----新】今天学习了基于JDK1.8的HashMap的源码,主要从如下几个方面来阐述,HashMap的数据结构,HashMap如何支持动态扩容,HashMap的散列函数是如何实现的,并且如何防止散列冲突,最后就是对HashMap的常用方法的源码解析。
目录
- HashMap的数据结构
- HashMap的散列函数
- 散列冲突的处理
- HashMap的扩容机制
- put 方法的源码解析
- get 方法和remove的源码解析
- 默认初始化的容器大小16:
static final int DEFAULT_INITIAL_CAPACITY = 1 < < 4; // aka 161 左移4位
- 最大的数据容量2的30次方。也就是说最多存放2的30次方个数据
static final int MAXIMUM_CAPACITY = 1 < < 30;
- 默认的加载因子 0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
PS: 散列表的加载因子=填入表中的元素个数/散列表的长度
加载因子越大,说明空闲位置越小,冲突越多,散列表的性能会下降。
4. 默认的链表转红黑树的链表长度
static final int TREEIFY_THRESHOLD = 8;
- 默认的红黑树转链表的红黑树节点个数
static final int UNTREEIFY_THRESHOLD = 6;
- 最小的链表树形化的table的大小。
static final int MIN_TREEIFY_CAPACITY = 64;
HashMap的数据结构(基于JDK1.8)HashMap的数据结构是散列表+链表+红黑树,其中散列表是其基本的数据结构(散列表使用的是桶数组,其实就是一个容量为N的普通数组A[0…N-1]。只不过,在这里我们要将每个单元都想象成一个"桶"(Bucket),每个桶单元里都可以存放一个条目。)。链表是用来存储散列值相同的结点的,当链表的默认长度大于8时链表就可能会转化成红黑树。
其数据结构图如下图所示:
从源码可知,HashMap类中有个非常重要的字段??
?Node<
K,V>
[] table?
?,即哈希桶数组,其实本质上就是一个数组。而Node是HashMap的一个内部类 ,实现了Map.Entry<
K,V>
接口,本质上就是一个键值对。static class Node< K,V> implements Map.Entry< K,V>
//用来定位数组索引位置(hash值)
final int hash;
//hash表的键
final K key;
//存储的值
V value;
//链表的下一个结点
Node< K,V> next;
HashMap的散列函数散列表中,我们需要一个函数,将任意键key转换为介于0与N-1之间的整数,这个函数就是散列函数(又称哈希函数),散列函数应该要满足如下三点基本要求:
- 散列函数计算得到的散列值必须是一个非负整数(因为数组的下标不可能是负数)
- 如果key1=key2, 那么hash(key1)=hash(key2)。
- 如果key1=/=key2, 那么hash(key1)=/=hash(key2)。
在HashMap中散列函数的实现如下:
static final int hash(Object key)
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h > > > 16);
如上代码我们可以看出hashMap的散列函数是通过调用key的hashCode()方法得到其hashCode值(该方法适用于所有java对象)。然后再通过hashCode值的高16位异或低16位(其中h > > > 16表示在二进制中将h右移16位)来得到hash值。
最后通过 ??
?(n - 1) &
hash;
?
??(n-1对hash值做按位与运算,也就是求模运算) 得到该键值对的存储位置 。下面举例说明,n为table的长度
散列冲突的处理当两个key定位到相同的位置是,就会发生散列冲突,散列函数计算结果越分数均匀,散列冲突的概率就会越小,map存储的效率就会越高。在HashMap中是通过链表法来处理,即位置相同的结点会存储到同位置上的链表上。具体的代码实现如下:
//遍历链表
for (int binCount = 0; ; ++binCount)
//当p.next (后继指针)为null时,则设置node结点
if ((e = p.next) == null)
p.next = newNode(hash, key, value, null);
//如果键和值已经存在则返回
if (e.hash == hash & &
((k = e.key) == key || (key != null & & key.equals(k))))
break;
p = e;
如上代码所示:散列冲突之后
- 首先遍历链表,在循环中,当p.next (后继指针)为null时,则设置node结点。
- 如果键和值已经存在则直接返回已经存在的数据。
int threshold; //map所能容纳的键值对的极限
final float loadFactor; //装载因子
int modCount; //记录HashMap被结构修改的次数,用于fast-fail
int size; //map中包含的key-value的个数
HashMap的构造器主要是给这几个属性设值。 正如前面说到的HashMap默认的容器大小(capacity)是16,默认的转载因子(loadFactor)是0.75,而 threshold=?
?loadFactor*capacity?
??,也就是说转载因子越大,map所能容纳的键值对就越多。 当HashMap中元素的个数超过threshold就会启动扩容,每次扩容都会扩容到原来的两倍大小。默认的装载因子0.75是对空间和效率的一种平衡选择,建议大家不要修改。而size 表示HashMap中实际存在的键值对数量,modCount字段主要是用来记录HashMap内部结构发生变化的次数,主要用于迭代快速失败。例如put新键值对,但是对某个key对应的value值覆盖不属于结构变化。其扩容主要分为如下两步:
- 创建一个新的两倍于原容量的数组。
- 循环将原数组中的数据移到新数组中。
具体代码如下:
final Node< K,V> [] resize()
Node< K,V> [] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0)//不是初始化,走扩容流程
//超过最大值就不在扩容
if (oldCap > = MAXIMUM_CAPACITY)
threshold = Integer.MAX_VALUE;
return oldTab;
//扩容后的容量是原来的2倍,左移一位就可以将数据翻倍
else if ((newCap = oldCap < < 1) < MAXIMUM_CAPACITY & &
oldCap > = DEFAULT_INITIAL_CAPACITY)
newThr = oldThr < < 1; //向左位移一位,达到原阀值的2倍。
else if (oldThr > 0) // 初始化容量,容量大小是threshold
newCap = oldThr;
else// zero initial threshold signifies using defaults
//容量,阀值指定初值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
//计算新的resize上限
if (newThr == 0)
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY & & ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
threshold = newThr;
//建立新容量数组
Node< K,V> [] newTab = (Node< K,V> [])new Node[newCap];
table =推荐阅读
- 数据仓库进阶 - 01 《阿里大数据之路》第二篇 数据模型篇
- #导入Word文档图片# Linux下IO多路复用: Selectpollepoll
- 流水线中如何获取代码库分支信息
- vivo大规模 Kubernetes 集群自动化运维实践
- H3CERG2系列路由器端口映射(虚拟服务器)
- kubernetes下的Nginx加Tomcat三部曲之二(细说开发)
- 银行卡API接口的功能与作用
- 关于数据一致性的理论
- 如何做一个泡泡龙游戏