HashMap 底层实现原理
两年前,我总觉得很多东西会用就行,不喜欢总结,不喜欢深入了解,这或许就是因为当时太懒。一年前,我觉得必须要把在工作积累到的东西、遇到的问题及解决方法给总结记录下来,以便快速提升自己,所以从那时候起就开始写 txt 文本,做一些简单记录。而至今,工作近三年,我越来越觉得了解底层原理的重要性。
HashMap本质:数组 + 链表
在JAVA数据结构中,常用数组和链表这两种结构来存储数据。
数组的存储区间(在内存的地址)是连续的,其大小固定,一旦分配就不能被其他引用占用,占用内存严重。数组的特点是:寻址容易,查询操作快,时间复杂度为O(1);但插入和删除的操作比较慢,时间复杂度是O(n)。
链表的存储区间是非连续(离散)的,其大小不固定,可以扩容,占用内存比较宽松,故空间复杂度很小。链表的特点是:寻址困难,查询速度慢,复杂度是O(n),插入快,时间复杂度为O(1)。
HashMap的数据结构:数组 + 链表(单链表),结合了两者的优点。HashMap的主干是一个Entry数组,数组每一个元素的初始值都是Null。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
HashMap的初始长度为16,且每次自动扩容或者手动初始化的时候必须是2的幂(以2次方增长)。所以,HashMap 的容量值都是 2^n 大小。
Entry是HashMap中的一个静态内部类。源码如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next; // 存储指向下一个Entry的引用,单链表结构
int hash; // 对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
HashMap
HashMap -- Put 方法实现
方法实现:将指定值与此映射中的指定键关联。如果映射以前包含了键的映射,则值被替换。
源码如下:
// 将指定值与此映射中的指定键关联。如果映射以前包含了键的映射,则值被替换。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 当key为null,调用putForNullKey方法,保存null于table第一个位置中,这是HashMap允许为null的原因
if (key == null)
return putForNullKey(value);
// 计算key的hash值
int hash = hash(key);
// 计算key hash 值在 table 数组中的位置
int i = indexFor(hash, table.length);
// 从i出开始迭代 e,找到 key 保存的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 判断该条链上是否有hash值相同的(key相同)
// 若存在相同,则直接覆盖value,返回旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; // 旧值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue; // 返回旧值
}
}
// 修改次数增加1
modCount++;
// 将key、value添加至i位置处
addEntry(hash, key, value, i);
return null;
}
例子 : hashMap.put(“clear”, 888)
首先计算key的hash值:int hash = hash(“clear”);
接着计算key hash 值在 table 数组中的位置:int i = indexFor(hash, table.length);
假定最后计算出的index是1,那么结果如下 :
由于HashMap的长度是有限的,当插入的Entry越来越多时,计算出来的index就会有重复,也就是hash冲突。hash值冲突的时候,就将对应节点以链表的形式存储。
头插法:新节点都增加到头部,新节点的next指向老节点;如下图中新的 Entry 2 指向旧的 Entry 1 。
HashMap通过键的hashCode来快速的存取元素。当不同的对象hashCode发生碰撞时,HashMap通过单链表来解决,将新元素加入链表表头,通过next指向原有的元素。
HashMap源码分析
HashMap的构造函数
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { ... }
HashMap实现了Map接口,继承AbstractMap。其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的骨干实现。
HashMap提供了三个构造函数:
HashMap():构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和指定加载因子的空 HashMap。
源码如下:
// HashMap的三个构造函数 -- 源码查看
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial capacity and the default load factor (0.75).
* 用指定的初始容量和默认负载因子(0.75)来构造空<TT> HashMap </TT>
* 如果初始容量为负值,则抛出非法的异常。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity (16) and the default load factor (0.75).
*使用默认初始容量(16)和默认负载因子(0.75)来构造空<TT> HashMap </TT>
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial capacity and load factor.
* 用指定的初始容量和负载系数 来构造空<TT> HashMap </TT>
* initialCapacity 设置的初始化容量,或者说是 HashMap 扩充数组时的阀值
* loadFactor 负载因子,默认时 0.75
* 如果初始容量为负值或负载因子为非正,则抛出非法逻辑异常
*/
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能<0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子不能 < 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
HashMap线程不安全原因
在多线程情况下,会导致hashmap出现链表闭环,一旦进入了闭环get数据,程序就会进入死循环,所以导致HashMap是非线程安全的。