朗坤研发 | HashMap底层实现原理
Posted 朗坤技术团队
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了朗坤研发 | HashMap底层实现原理相关的知识,希望对你有一定的参考价值。
点击上面蓝字朗坤研发中心 ▲ 订阅
内容来源:平台研发部--李重阳 推荐
通过查看源码进行分析,即通过查看HashMap.class
JDK 1.6.0_45
1、HashMap类
HashMap继承了AbstaractMap
AbstractMap实现了Map接口(AbstarctMap中实现了Map中常用/常见方法)
HashTable提供了Map接口所有可选的实现,并且语序key和vaule为null,HashMap基本功能和HashTable相同,都允许key和value为null,但是HashMap是非线程安全的。同时不能保证Entry的顺序
Hash假设能够将Entry分配到合适的bin中,put和get的时间复杂量为常量。遍历key或value和Entry的时间复杂度为HashMap的capactity + Entry的数量有关,如果对遍历Entry有一定性能的要求,那么不能将capacity设置的太高或者load factory太低
HashMap有两个参数初始化:capacity,load factor,可能会影响到它的性能,capacity决定HashTable的bin数量,load factor是一个衡量是否需要增加capacity的标准,当Entry的数量超过capacity 或者load factor时,则会rehashed,内部的数据结构将会重建,以保证hash table拥有2倍的buckets
load factor默认为0.75,它能够在时间和性能方面,提供一个折中。当空间负载越多,消耗的时间越多。在get和put的操作上,当我们设置初始化量capatity时,应该要考虑会有多少Entry,以及负载因子load factory,减少rehash的可能。如果实际的Entry容量达不到 capacity * load factor,将不会rehashed
2、HashMap成员变量
DEFAULT_INITIAL_CAPACITY
/**
The default initial capacity - MUST be a power of two.
默认初始化容量-必须是2的幂(即:必须是2的n次方),默认是16
/ static final int DEFAULT_INITIAL_CAPACITY = 16; MAXIMUM_CAPACITY /*The maximum capacity, used if a higher value is implicitly specified
by either of the constructors with arguments.
MUST be a power of two <= 1<<30.
任何一个构造函数隐式指定了一个具有参数的值,则使用该最大容量
必须小于或者等于2的30次方(1<<30,表示 1 * 2的30次方 )
即:new HashMap的时候,容量不得超过2的30次方
/ static final int MAXIMUM_CAPACITY = 1 << 30; DEFAULT_LOAD_FACTOR /*The load factor used when none specified in constructor.
在构造函数中没有指定的负载因素的时候,使用这个成员变量(默认加载因子)
/ static final float DEFAULT_LOAD_FACTOR = 0.75f; Entry[] table /*The table, resized as necessary. Length MUST Always be a power of two.
Entry类型的数组,HashMap用这个来维护内部的数据结构,长度必须是2的n次方
/ transient Entry[] table; int size /*The number of key-value mappings contained in this map.
在map中key-value映射数量(HashMap的大小)
/ transient int size; threshold /*The next size value at which to resize (capacity * load factor).
@serial
下次扩容的临界值,大小>=threshold(容量和 * 加载因子),就会扩容
HashMap的极限容量
/ int threshold; loadFactor /*The load factor for the hash table.
哈希表的加载因子
@serial
/ final float loadFactor; modCount /*The number of times this HashMap has been structurally modified
Structural modifications are those that change the number of mappings in
the HashMap or otherwise modify its internal structure (e.g.,
rehash). This field is used to make iterators on Collection-views of
the HashMap fail-fast. (See ConcurrentModificationException).
*HashMap结构修改的次数,结构性的修改是指,改变Entry的数量
/ transient volatile int modCount; 3、HashMap构造函数 HashMap() /*Constructs an empty HashMap with the default initial capacity
(16) and the default load factor (0.75).
构造一个具有默认初始容量(16)和默认加载因子(0.75)的空HashMap
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
HashMap(int initialCapacity)
/**Constructs an empty HashMap with the specified initial
capacity and the default load factor (0.75).
构造一个指定容量和默认加载因子(0.75)的空HashMap
*@param initialCapacity the initial capacity.
@throws IllegalArgumentException if the initial capacity is negative.
/ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } HashMap(int initialCapacity, float loadFactor) /*Constructs an empty HashMap with the specified initial
capacity and load factor.
构造一个带有指定容量和指定加载因子的空HashMap
*@param initialCapacity the initial capacity
@param loadFactor the load factor
@throws IllegalArgumentException if the initial capacity is negative
or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
//初始化容量<0,抛出异常 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //初始化容量>最大容量,默认使用最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子<=0或者为空,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);// Find a power of 2 >= initialCapacity
int capacity = 1;
//初始化容量,做了一个移位运算,假设传入5,最终初始化容量为8
while (capacity < initialCapacity)
capacity <<= 1;//capacity = capacity << 1,乘以2的1次方this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
HashMap(Map
/**Constructs a new HashMap with the same mappings as the
specified Map. The HashMap is created with
default load factor (0.75) and an initial capacity sufficient to
hold the mappings in the specified Map.
*构建一个映射关系和指定Mpa相同的,新HashMap,默认初始化容量16,初始化加载因子0.75
*@param m the map whose mappings are to be placed in this map
@throws NullPointerException if the specified map is null
/ public HashMap(Map m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAllForCreate(m); } 4、HashMap容量/数据结构 从第三小节中可以发现,所有源码最终使用的构造函数为HashMap(int initialCapacity, float loadFactor) 而在HashMap(int initialCapacity, float loadFactor)构造函数中,我们来仔细看看源码 public HashMap(int initialCapacity, float loadFactor) { /*//初始化容量<0,抛出异常 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //初始化容量>最大容量,默认使用最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//加载因子<=0或者为空,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);// Find a power of 2 >= initialCapacity
int capacity = 1;
//初始化容量,做了一个移位运算,假设传入5,最终初始化容量为8
while (capacity < initialCapacity)
capacity <<= 1;//capacity = capacity << 1,乘以2的1次方this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
//计算出capacity的值,创建Entry数组
table = new Entry[capacity];
init();
}
while (capacity < initialCapacity),这句代码使用了移位运算,有效保证了HashMap的初始化容量始终为2的幂
那么,为什么HashMap容量一定要为2的幂呢?这个构造函数主要做的事情:
1.对传入的初始化容量、加载因子进行校验处理
2.计算出大于初始化容量的最小2的N次方作为哈希表table的长度,再利用该长度创建Entry数组
*/
HashMap中的数据结构是:数组 + 单列表,我们希望的是:元素存放的更加均匀,最理想的时候,Entry数组中每一个位置中只有一个元素,这样,查询的时候效率最高,不需要遍历单列表,也不需要通过equals去比较K,而且空间利用率最大,时间复杂度最低
下面来看看数据结构
从上图可以更容易发现,HashMap由 数组+链表 组成,每一个元素存储的是一个链表的头结点
那么,元素按照什么规则存储到数组中呢?
一般是通过 hash(key)%len获得,也就是元素的key的哈希值对数组的长度取模得到,如:12%4=0,13%4=1,17%4=1,21%4=1,所以13,17,21存储在Entry[1]中
下面来看看Entry数组的结构
static class Entry
/*
* Entry是HashMap的内部类,它维护这一个Key-value映射关系
* next:引用指向当前table位置的链表
* hash值:用来确定每一个Entry链表在table中位置
*/
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* 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 (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
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) {
}
}
5、HashMap实现存储
public V put(K key, V value) {
//如果key为空
if (key == null)
//如果为null,则调用putForNullKey:这就是为什么HashMap可以用null作为键的原因
return putForNullKey(value);
//计算key的hash值
int hash = hash(key.hashCode());
//计算该hash值在table中的下标
int i = indexFor(hash, table.length);
//对table[i]存放的链表尽心遍历
for (Entry
Object k;
//判断该条链上是否有hash值相同的(key相同)
//若存在相同,则直接覆盖value,返回旧value
//这就是为什么HashMap不能有两个相同的key的原因
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改次数
modCount++;
//把当前key,value添加到table[i]的链表中
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
//查找链表中,是否有null键
for (Entry
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果链中查找不到,则把该null键插入
addEntry(0, null, value, 0);
return null;
}
static int hash(int h) {
//^异或(同为0,异为1) >>>转化为二进制右移位,不足补0
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
//对于HashMap的table而言,数据分布需要均匀
//怎么才能保证table元素分布均与呢?我们会想到取模,但是由于取模的消耗较大
//而HashMap是通过&运算符(按位与操作)来实现的
//
以上是关于朗坤研发 | HashMap底层实现原理的主要内容,如果未能解决你的问题,请参考以下文章