HashMap
Posted jxkun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap相关的知识,希望对你有一定的参考价值。
目录
HashMap
- HashMap jdk7与8的实现存在差异;
- 在HashMap7的基础上, 8对7进行了优化;
一、HashMap(jdk7)
1.1 HashMap的结构
- HashMap 里面是一个
Entry<K,V>[]
类型数组table
,然后数组中每个元素是一个单向链表。
上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。 - capacity:当前数组容量,始终保持 2^n,最大容量为2^30,可以扩容,扩容后数组大小为当前的 2 倍。默认容量(DEFAULT_INITIAL_CAPACITY) 为16;
- loadFactor:负载因子,默认负载因子(DEFAULT_LOAD_FACTOR)为 0.75。
- threshold:扩容的阈值,等于 capacity * loadFactor
- size属性, 记录Entry实例的数量;
- modCount 属性, 记录HashMap实例修改的次数;
1.2 Entry节点的结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash; // 存key的hash值, key为null, 则为0
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
- Entry节点为一个单向链表的结构;
- Entry节点重写了HashCode方法, hashCode值为key的hash值与value的hash值向异或;
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
- Entry节点的equals方法, 先查看比较的Object是否为Map.Entry类型, 若是则先比较key是否相等, 若相等则比较value是否相等;
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;
}
1.3 hash(Object obj)
多次移位运算, 目的是为了让hash值更均匀的分布在hash表中;
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
1.4 put方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<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++; // 记录HashMap实例的变化次数
addEntry(hash, key, value, i);
return null;
}
- 校验key是否为null, key为null则遍历table[0]处的链表, 若链表不为空则将value更新, 返回旧的value; 若链表为空, 则创建Entry节点,并将其插入到table[0]头部;具体创建Entry及插入步骤参考4
key不为null, 先获取key的hash值, 通过hash值与map长度-1相与, 获得key在table中的索引, 遍历该索引处的链表, 若链表中存在相同的key, 则更新对应的value, 将旧value返回;
判断key相同的条件: e.hash == hash && ((k = e.key) == key || key.equals(k)); 先比较hash值, 再比较是否相等;
若索引链表没有相同的key, 调用创建Entry实例, 将Entry实例插入到链表头部; 具体创建及插入步骤参考4
先检查是否需要扩容, 若需要扩容则先扩容,扩容之后重新计算key的hash值,索引值, 创建Entry节点然后插入到链表头部;
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
1.5 HashMap扩容
扩容条件: 在put新的key-value是, Entry节点的数量size大于capacity与loadFactor乘积threshold, 且恰好key所在的索引处已经存在节点了, 于是触发扩容
(size >= threshold) && (null != table[bucketIndex])
HashMap中每次扩容为之前的两倍, 然后将table中的节点放到新的newTable中,将table更新为newTable;
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
// 遍历hash表table, 将 entry节点放到新的 newTable中;
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
// 重新计算索引值
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
1.6 get方法
get方法获取key值对应的value;
若key为null, 则直接从table[0] 中查找, 若table0出链表不为空, 则返回value值;
key不为null, 计算出key的hash值, 通过hash值与map长度-1相异或获取key在table中的索引, 遍历索引处的链表, 若找到一个相等的key则将其Entry返回;若没找到则返回null;
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<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;
}
return null;
}
1.7 问题汇总
问题1: put过程中为什么entry节点要从头部插入?
在多线程情况下, 如果一个线程a正在get key1, 当线程a正在遍历链表时, 另一线程b在put key2, 而恰好key1, key2在table中的索引相同, 若put将key2-value插入到链表尾部, 则会被线程a所访问到, 产生数据不一致问题; 而若put将key2-value插入到链表头部, 则不会存在这个问题, 在一定程度上保证线程安全;
问题2: 扩容过程中entry节点位置的变化规律?
每次扩容, 会将map的length翻倍, 而key在table中的索引是由 hash & (length - 1) 计算出来的, 相比而言每次扩容索引值相当于hash值多暴露一bit位, 若暴露的bit位为0, 则索引与之前相比不发生变化, 若暴露的bit位为1, 则索引与之前相比则右移了length/2长度(length指的是扩容后的长度);
问题3: HashMap的put操作存在的线程安全问题?
主要有两个线程安全问题:
- put时, entry节点被覆盖: 当线程a, b同时执行完获取索引头结点后, 线程a抢到执行CPU权, 向下执行将自己设置为头结点, a执行结束; b执行, 由于索引处头结点并没有更新, b将自己的entry设置为头结点, 此时将a线程设置的头结点覆盖, a线程put操作key-value丢失;
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; // 先获取索引处头结点, 线程a, 线程b; // 将头结点设置为为新entry的next节点 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
扩容时链表成环:
参考: https://blog.csdn.net/hhx0626/article/details/54024222
二、HashMap(jdk8)
HashMap在jdk8, 使用数组 + 链表 + 红黑树的方式组成, 在 Java8 中,当链表中的元素超过了8个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。
2.1 Node 与 TreeNode
- Node节点, 与jdk7实现的Entry节点类型; 是一个单向链表节点的结构;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
- TreeNode节点, 继承与LinkedHashMap.Entry<K,V>, 而LinkedHashMap.Entry<K,V>继承于HashMap.Node<K,V>, 因此TreeNode为Node类的子类; 为一个红黑树节点结构;
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
2.2 HashMap新增的几个属性
static final int TREEIFY_THRESHOLD = 8; //
static final int UNTREEIFY_THRESHOLD = 6; //
static final int MIN_TREEIFY_CAPACITY = 64; //
transient Node<K,V>[] table; // Node数组, 存储key-value对的hash表; jdk1.7时, 为Entry[]类型
2.3 hash方法
HashMap在jdk8中hash值计算,采用key的hash值与key的hash值的高16位相异或, 好处在于: key在数组中的索引是通过hash & (length - 1) 计算出来的, 计算key的hash值时采用高位与低位相异或, 让高位也能尽可能影响到索引的计算, 可以使key在数组中分布的更均匀;
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
2.4 get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 根据key获取对应Node节点;
* @param hash key的hash值, hash(key)
* @param key key对象
* @return key所在的Node节点;
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // 先检查头结点, key若在头结点直接返回;
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) { // 不在头结点, 遍历后面的节点
// 若节点类型为TreeNode, 为红黑树结构调用方法获取TreeNode
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do { // 节点类型不为TreeNode, 为链表结构, 则遍历后面的节点查找;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
2.5 put方法
- 计算出key所在的索引, 若此索引处为null, 则创建节点直接添加到索引处; 不为null, 则走2
- 检查索引头结点处的key是否与要插入的key-value对的key相等, 若想等则将之前的旧值更新, 返回旧value; 若不等走3
- 检查头结点类型是否为TreeNode, 若是, 则调用TreeNode的putTreeVal方法插入key-value; 若否则走4
- 循环遍历, 使用一个binCount变量计数, 若key与其中的Node节点key相等,则退出循环,走5; 若不相等, 遍历到尾节点, 将key-value插入到尾节点, 检查binCount的值, 若binCount大于等于7, 则将这条链表转换成红黑树结构;
- 若e不为null, 则返回e.value, 即旧的key值; 若e为null, 则++size, 判断size是否大于等于threshold, 若为true则进行扩容;
- put方法特点, 先添加节点后检查是否要扩容; 而jdk7则是先检查扩容, 后添加节点;
- jdk8新节点插入到链表末尾, 而jdk7新节点插入到链表头部;
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 当binCount大于等于7时, 将tab中哈希值为hash对应索引处的链表转换为红黑树结构
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// size大于等于threshold则扩容;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
以上是关于HashMap的主要内容,如果未能解决你的问题,请参考以下文章