引言
在JAVA / Android开发中,或多或少会使用到HashMap 来存取数据,趁着其在JDK 8.0 更新了实现方式,本篇先来解析一下其在JDK 7.0 工作原理。
文末有疑问?
如有问题,还望指出,谢谢
实现原理
知识点: 数组 + 链表 + hash运算
HashMap = 数组 + 单链表 = 拉链法
数组
index:key值计算得出的hash值
value:HashMapEntry 对象,为链表头节点
length:HashMap 容量
链表
HashMapEntry : 当计算得出hash值相同时,使用链表方式存放键值对
特点 - 文末有解答
1. 键值对可为空
2. 非线程安全
3. 不保证有序
源码解析
链表
设置节点类HashMapEntry,每个节点里面包含key、value、next、hash
其中next 用来存放指向的下一个HashMapEntry对象
作用:解决hash冲突 存储计算得出的hash值相同的键值对,链地址法
//Entry类实现了Map.Entry接口
static class HashMapEntry<K,V> implements Map.Entry<K,V> {
final K key; //键
V value; //值
HashMapEntry<K,V> next; //存放指向的下一个HashMapEntry对象
int hash; //key 计算出的hash值
/**
* Creates new entry.
*/
HashMapEntry(int h, K k, V v, HashMapEntry<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;
}
/**
* equals()
* 作用:判断2个Entry是否相等,必须key和value都相等,才返回true
*/
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;
}
/**
* hashCode()
*/
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* 当向HashMap中添加元素时,即调用put(k,v)时,
* 对已经在HashMap中k位置进行v的覆盖时,会调用此方法
* 此处没做任何处理
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* 当从HashMap中删除了一个Entry时,会调用该函数
* 此处没做任何处理
*/
void recordRemoval(HashMap<K,V> m) {
}
}
put
作用:存储数据
流程图:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold); //初始化容量
}
if (key == null)
return putForNullKey(value);//存key为空的值
//如果key 不为空,计算Hash 值
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//计算存储位置i,如果i位置已经存在链表,则遍历链表更换hash key 对应的value
int i = indexFor(hash, table.length);
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果数组中i位置没有值或者链表中没有对应的key值,则在i位置添加数据
addEntry(hash, key, value, i);
return null;
}
添加key 为null 的键值对
private V putForNullKey(V value) {
//遍历table[0] 中的链表进行查询;如果有值,则替换;否则重新添加
for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//如果数组中0位置没有值或者链表中没有对应的key值,则在0位置添加数据
addEntry(0, null, value, 0);
return null;
}
插入新数据
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果size 到了阀值,并且要添加的位置不为空,那么需要扩容,并重新计算hash 以及 index
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//获取index 位置链表头部数据
HashMapEntry<K,V> e = table[bucketIndex];
//将添加的放到链表头部,原来的头部成为新头部的next
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
//数量加1
size++;
}
get
作用:获取数据
public V get(Object key) {
if (key == null)
return getForNullKey(); //判断key 是否为空
//获得key 对应的 HashMapEntry值
Entry<K,V> entry = getEntry(key);
//判断链表节点不为空时,返回该节点的value
return null == entry ? null : entry.getValue();
}
获取key 为null 的值
private V getForNullKey() {
//Hashmap 中数据为空,直接返回null
if (size == 0) {
return null;
}
//遍历位置为0上链表,找到key为null 的值返回
for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null; //找不到返回null
}
获取key 为 不为null 的值
final Entry<K,V> getEntry(Object key) {
if (size == 0) { //Hashmap 中数据为空,直接返回null
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key); //第一次计算hash值
//第二次计算hash值得到index后,遍历index位置的链表,当hash key相同时,返回链表节点
for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
//链表中找不到,返回null
return null;
}
size
作用:HashMap 中所有键值对的数量
put时size++;remove时,size–
public int size() {
return size;
}
clear
作用:清除数据
实际就是将数组的值置为null
public void clear() {
modCount++;
Arrays.fill(table, null);
size = 0;
}
isEmpty
作用:判断HashMap是否为空
public boolean isEmpty() {
return size == 0;
}
containsKey
作用:判断HashMap是否存在该key
与get 步骤类似
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
containsValue
作用:判断HashMap是否存在该Value
遍历数组以及链表中所有元素,查看对应value
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
HashMapEntry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (HashMapEntry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
remove
作用:删除某key
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.getValue());
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
HashMapEntry<K,V> prev = table[i];
HashMapEntry<K,V> e = prev;
while (e != null) {
HashMapEntry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next; //链表头就是,那么next为头
else
prev.next = next; //否则,断掉中间的
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
replace
作用:替换值
查找key值对应的value 进行替换
@Override
public boolean replace(K key, V oldValue, V newValue) {
HashMapEntry<K,V> e; V v;
if ((e = (HashMapEntry)getEntry(key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
e.recordAccess(this);
return true;
}
return false;
}
疑问
如何计算hash值?如何减少hash冲突?
Android 中
public static int singleWordWangJenkinsHash(Object k) {
int h = k.hashCode();
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
Java 中
// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//按位 与 运算,都为1 才为1,否则为0
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
为什么不直接用HashCode? 为什么要 h & (length-1)?
这边其实是一个问题,如果直接使用HashCode, 可能会出现越界, 而通过h & (length-1) 可以保证其在容量范围内,取容量范围的低位作为索引。
为什么不直接用HashCode? 为什么要 h & (length-1)?
1、好的hash运算 Android / Java 不同
2、二次运算,加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突
3、使用链地址法方式存储相同hash值的键值对
键值对可为空、非线程安全、不保证有序 ?
键值对可为null:当key 为null 后,获取的hash 值为0,所以默认存储在table[0]位置上
不保证有序:数组下标,是通过key值经过hash 和 容量运算得出,所以不是有序的
非线程安全:当多个线程同时操作同一个HashMap时,无法同步数据
比如当多线程同时put 值,需要扩容时,可能会导致死循环
线程1:
本来链表顺序A->B->C
在进行到扩容时,B->A 停止调度,执行线程2
线程2:
前提:现在链表顺序B->A
在进行扩容时,变成A->B
此时会出现A和B之间出现互相指向next,导致循环
如何扩容?
扩容前提:当size >= 阀值 || putAll的HashMap的容量大于table.length
//传入新的容量值
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果老值已经是最大的 1<<30 2^30,则扩容失败
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建新table
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//切换table,将链表中的值都赋值到newTable中
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
//e的值存入新计算出index的位置上,并将此链表中的键值对都赋值一下
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
注意:扩容后,链表相比较上次链表为倒序。
1->2->3 变成 3->2->1
容量 阀值 加载因子 已存数量 的联系和区别?
容量table:数组长度
阀值threshold:控制扩容的值 阀值 = 容量 * 加载因子, 决定了HashMap 容量里面存放多少键值对
加载因子loadFactor:当容量一定时,加载因子决定了阀值
已存容量size:数组和链表中所有键值对之和,当该值>=阀值,需要扩容
思考,加载因子用处,代码默认0.75
当容量为100,加载因子0.75,阀值为75.
当容量为100,加载因子0.15,阀值为15.
由此可见当加载因为越大,空间使用率越高,但是出现Hash冲突概率高,从而导致链表过长,进而导致查询效率变低
当加载因子越小,空间使用率越低,但是出现Hash冲突概率低,链表不会长,查询效率越高(就像直接查询数组一样)
使用建议:看设备空间和查询效率更看重哪个。一般不做修改,默认0.75
为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键 ?
final 类型,具有不可变性,保证key 的不可更改, 可有效减少Hash 冲突