HashMap是开发过程中经常用到的数据结构,因为其key-value存储方式,方便了数据的存储与获取,但是一直都不知道其内部实现逻辑与底层原理,所以决定深入的学习一番。
HashMap是哈希表(散列表),是较为重要的数据结构,因为本人平常使用的是jdk1.7,所以本文将会对jdk1.7的HashMap源码进行分析。
一:哈希表定义
我们先来看看数组和链表在新增和查询等操作方面的执行性能如何?
数组: 采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),当然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn);对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
链表: 对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
哈希表是数组和链表组成的一种数据结构,它同时拥有数组和链表的优点,哈希表的主干是数组,比如我们要新增或查找某个元素,我们通过把当前元素的关键字(key)通过某个函数映射到数组中的某个位置,通过数组下标一次定位就可完成操作。
存储位置 = f(key),这里的f()就是哈希函数
这样我们就是使用到了哈希表的数组主干,但是链表结构我们何时能用到呢?
哈希碰撞(哈希冲突):
当两个不同的元素通过哈希函数计算后得到的数据存储地址是一样的,即当我们准备插入新元素的时候,通过哈希函数运算得出的存储地址已经被其他元素占用了,这就是哈希碰撞。
解决哈希碰撞的方法有以下这几种:
开放定址法:发生冲突,继续寻找下一块未被占用的存储地址
再散列函数法:第一次散列产生哈希地址冲突,为了解决冲突,采用另外的散列函数或者对冲突结果进行处理的方法
链地址法:HashMap使用到的哈希碰撞的解决方案就是链地址法,当产生哈希冲突的时候加链表来处理。
二:HashMap的实现原理
数据结构示意图:
数据结构
成员变量
static final int DEFAULT_INITIAL_CAPACITY = 16; 默认初始化容量大小
static final int MAXIMUM_CAPACITY = 1073741824; 最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75F; 默认加载因子(默认为0.75f)
transient Entry<K, V>[] table; Entry类型的数组,HashMap用这个来维护内部的数据结构,它的长度由容量决定
transient int size; HashMap大小
int threshold; 临界阈值
final float loadFactor; 加载因子
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = 2147483647;
transient boolean useAltHashing;
final transient int hashSeed; hash因子
四个构造函数
public HashMap(int capacity, float loadFactor);
public HashMap(int capacity);
public HashMap();
public HashMap(Map<? extends K, ? extends V> paramMap);
以上有两个比较重要的参数,capacity(容量)和loadFactor(加载因子)
容量:记录当前HashMap中Entry的数量,也就是table的容量大小
加载因子:代表table的填充度,默认是0.75
其中public HashMap(int capacity, float loadFactor)是核心构造函数
可以看到HashMap构造函数中主要对capacity和loadFactor做了一些控制,其中 i 是以capacity作为标准,进行多次乘方,直到 i 大于等于capacity,保证了 i 一定是2的次幂,最后以 i 作为容量大小初始化了table这个Entry数组,同时也初始化了threshold(临界阈值)。
public HashMap(int capacity, float loadFactor)
{
this.hashSeed = Hashing.randomHashSeed(this);
this.entrySet = null;
//当传入的容量值小于0,抛错
if (capacity< 0) {
throw new IllegalArgumentException("Illegal initial capacity: " + capacity);
}
//当传入的容量值大于容量的最大值,将最大值赋给容量变量
if (capacity> 1073741824)
capacity= 1073741824;
//若容量值小于0或者容量值不是一个数字,抛错
if ((loadFactor<= 0.0F) || (Float.isNaN(loadFactor))) {
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
}
//初始化一个i为1
int i = 1;
//循环比较i和capacity,若i小于capacity,将i向左移一位,也就是乘以2,使i始终为2的n次方
while (i < capacity) {
i <<= 1;
}
this.loadFactor = loadFactor;
//给临界阈值赋值,容量*加载因子 和 1.073742E+009F 之中的较小者
this.threshold = (int)Math.min(i * loadFactor, 1.073742E+009F);
//为HashMap表创建Entyr数组
this.table = new Entry[i];
this.useAltHashing = ((VM.isBooted()) && (i >= Holder.ALTERNATIVE_HASHING_THRESHOLD));
//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
init();
}
Entry结构
Entry的key和value分别对应我们存入HashMap中的键和值,next存储下一个链表中下一个Entry的引用,hash存储key对应的hash值,用于确定该Entry在Entry数组中的位置。
static class Entry<K, V>
implements Map.Entry<K, V>
{
final K key;
V value;
Entry<K, V> next;
int hash;
Entry(int paramInt, K paramK, V paramV, Entry<K, V> paramEntry)
{
this.value = paramV;
this.next = paramEntry;
this.key = paramK;
this.hash = paramInt;
}
我们再来看看put方法:
public V put(K paramK, V paramV)
{
if (paramK == null)
return putForNullKey(paramV);
//对传入的key进行哈希运算,得到hash值
int i = hash(paramK);
//根据计算出来的哈希值 i 和表的长度得到Entry在数组中的存储位置
int j = indexFor(i, this.table.length);
//如果传入的key所对应数组中的位置已经被占用了,那么进行覆盖操作
for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next)
{
Object localObject1;
if ((localEntry.hash == i) && ((((localObject1 = localEntry.key) == paramK) || (paramK.equals(localObject1))))) {
Object localObject2 = localEntry.value;
localEntry.value = paramV;
localEntry.recordAccess(this);
return localObject2;
}
}
//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
this.modCount += 1;
addEntry(i, paramK, paramV, j);
return null;
}
put方法中的int hash(paramK);
hash函数中对于传入的key的hashCode进行一系列的计算来保证最终获取的存储位置尽可能的分布均匀。
final int hash(Object paramObject)
{
int i = 0;
if (this.useAltHashing) {
if (paramObject instanceof String) {
return Hashing.stringHash32((String)paramObject);
}
i = this.hashSeed;
}
i ^= paramObject.hashCode();
i ^= i >>> 20 ^ i >>> 12;
return (i ^ i >>> 7 ^ i >>> 4);
}
put方法中的 int j = indexFor(i, this.table.length);
根据hash函数计算出的hash值和HashMap容量大小计算出具体存储位置下标
static int indexFor(int paramInt1, int paramInt2)
{
return (paramInt1 & paramInt2 - 1);
}
put方法中的 addEntry(i,paramK,paramV,j);
void addEntry(int paramInt1, K paramK, V paramV, int paramInt2)
{
//在新增Entry对象之前先检查HashMap容量,若当前容量大于临界阈值并且将要插入的存储位置已经被占
//用,将要发生哈希冲突时,进行扩容,扩大的容量是之前的两倍
if ((this.size >= this.threshold) && (null != this.table[paramInt2])) {
resize(2 * this.table.length);
paramInt1 = (null != paramK) ? hash(paramK) : 0;
paramInt2 = indexFor(paramInt1, this.table.length);
}
//新建Entry
createEntry(paramInt1, paramK, paramV, paramInt2);
}
addEntry中的createEntry(paramInt1, paramK, paramV, paramInt2);
void createEntry(int paramInt1, K paramK, V paramV, int paramInt2)
{
//首先获取到当前Entry对象
Entry localEntry = this.table[paramInt2];
//获取Entry头结点,并创建新节点,把该新节点插入到链表中的头部,该新节点的next指针指向原来的头结
//点
this.table[paramInt2] = new Entry(paramInt1, paramK, paramV, localEntry);
//HashMap当前容量+1
this.size += 1;
}
至此,HashMap中已经新增了一个元素。
让我们再看看key值为NULL的情况下,如何进行存储?
private V putForNullKey(V paramV)
{
for (Entry localEntry = this.table[0]; localEntry != null; localEntry = localEntry.next) {
if (localEntry.key == null) {
Object localObject = localEntry.value;
localEntry.value = paramV;
localEntry.recordAccess(this);
return localObject;
}
}
this.modCount += 1;
addEntry(0, null, paramV, 0);
return null;
}
上述代码,开始遍历Entry数组中位置为0的链表,是否key值为null的存储位置已经被占用了,如果被占用了,则覆盖。否则在数组0位置上新增Entry。
接下来看看 get方法
public V get(Object paramObject)
{
if (paramObject == null)
return getForNullKey();
Entry localEntry = getEntry(paramObject);
return ((null == localEntry) ? null : localEntry.getValue());
}
如果传入的key值为null,则取数组位置为0的Entry,否则调用getEntry(paramObject)
final Entry<K, V> getEntry(Object paramObject)
{
//获取hash值
int i = (paramObject == null) ? 0 : hash(paramObject);
//获取实际存储位置
Entry localEntry = this.table[indexFor(i, this.table.length)];
//开始比对localEntry开头的链表,查询key值对应的Entry
while (localEntry != null)
{
Object localObject;
if ((localEntry.hash == i) && ((((localObject = localEntry.key) == paramObject) || ((paramObject != null) && (paramObject.equals(localObject))))))
{
return localEntry;
}
//如果没找到,则将当前Entry引用设置为链表下一个,继续比对
localEntry = localEntry.next;
}
return null;
}
以上便是HashMap实现原理,介绍了哈希冲突以及散列表数据结构,还有HashMap里的构造函数、put和get方法。
三:重写equals方法需同时重写hashCode方法
//获取hash值
int i = (paramObject == null) ? 0 : hash(paramObject);
//获取实际存储位置
Entry localEntry = this.table[indexFor(i, this.table.length)];
让我们再来看一看之前get方法里的一段代码,hash值会影响之后localEntry实际存储位置的运算,hash值运算有误的话,那么就找不到正确的存储地址,得不到正确的Entry。
i ^= paramObject.hashCode();
而hash值是由key值的hasCode值生成的
//开始比对localEntry开头的链表,查询key值对应的Entry
while (localEntry != null)
{
Object localObject;
if ((localEntry.hash == i) && ((((localObject = localEntry.key) == paramObject)
|| ((paramObject != null) && (paramObject.equals(localObject))))))
{
return localEntry;
}
判断一个Entry是否匹配,有这么四个判断条件:hash值、key值、是否为空、调用equals是否为true,若我们只修改equals方法的话,虽然不同的对象在put和get方法中的key值通过equals方法比对是相等的,但是hash值会因为hasCode方法没有修改而不同,导致在取值的时候,最终计算出的真实存储位置不一致,所以get方法获取不到值,因为他们的hash值和真实存储位置都有可能不同。