1. 前言
本文的源码是基于 JDK1.7,JDK1.8 中 HashMap 的实现。
2. 用法
2.1 基本用法
HashMap 很方便地为我们提供了 key-value 的形式存取数据,使用 put 方法存数据,get 方法取数据。
Map hashMap = new HashMap();
hashMap.put("key", "value");
String name = hashMap.get("key");
2.2 定义
HashMap 继承了 Map 接口,实现了 Serializable 等接口。
在JDK8中,当链表长度达到8,会转化成红黑树,以提升它的查询、插入效率,它实现了Map
public class HashMap extends AbstractMap implements Map, Cloneable, Serializable{
*
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
}
其实 HashMap 的数据是存在 table 数组中的,它是一个 Entry 数组,Entry 是 HashMap 的一个静态内部类,看看它的定义。
static class Entry implements Map.Entry {
final K key;
V value;
Entry next;
int hash;
可见,Entry 其实就是封装了key 和 value,也就是我们 put 方法参数的 key 和 value 会被封装成 Entry,然后放到 table 这个 Entry 数组中。但值得注意的是,它有一个类型为 Entry 的 next
,它是用于指向下一个 Entry 的引用,所以 table 中存储的是 Entry 的单向链表。默认参数的HashMap结构如下图所示:
2.3 构造方法
HashMap 一共有四个构造方法,我们只看默认的构造方法。
/**
* Constructs an empty HashMap with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 找到第一个大于等于initialCapacity的2的平方的数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
// HashMap扩容的阀值,值为HashMap的当前容量 * 负载因子,默认为12 = 16 * 0.75
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化table数组,这是HashMap真实的存储容器
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
// 该方法为空实现,主要是给子类去实现
init();
}
initialCapacity
是 HashMap 的初始化容量(即初始化 table 时用到),默认为 16。
loadFactor
为负载因子,默认为 0.75。
threshold
是 HashMap 进行扩容的阀值,当 HashMap 的存放的元素个数超过该值时,会进行扩容,它的值为 HashMap 的容量乘以负载因子。比如,HashMap 的默认阀值为 16 * 0.75,即 12。
HashMap 提供了指定 HashMap 初始容量 和 负载因子 的构造函数,这时候会首先找到第一个大于等于 initialCapacity 的 2 的平方数,用于作为初始化 table。至于为什么 HashMap 的容量总是 2 的平方数,后面会说到。
继续看 HashMap 构造方法,init 是个空方法,主要给子类实现,比如LinkedHashMap 在 init 初始化头部节点。
2.4 put 方法
public V put(K key, V value) {
// 对key为null的处理
if (key == null)
return putForNullKey(value);
// 根据key算出hash值
int hash = hash(key);
// 根据hash值和HashMap容量算出在table中应该存储的下标i
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
// 先判断hash值是否一样,如果一样,再判断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++;
addEntry(hash, key, value, i);
return null;
}
首先,如果 key 为 null 调用 putForNullKey 来处理,我们暂时先不关注,后面会讲到。然后调用 hash 方法,根据 key 来算得 hash 值,得到 hash 值以后,调用 indexFor 方法,去算出当前值在 table 数组的下标,我们可以来看看 indexFor 方法:
static int indexFor(int h, int length) {
return h & (length-1);
}
与运算替代模运算。用 hash & (table.length-1) 替代 hash % (table.length)。
这其实就是 mod 取余的一种替换方式,相当于 h % lenght,其中 h 为 hash 值,length 为 HashMap 的当前长度。而 & 是位运算,效率要高于 %。至于为什么是跟 length-1 进行 & 的位运算,是因为 length 为 2 的幂次方,即一定是偶数,偶数减 1,即是奇数,这样保证了(length-1)在二进制中最低位是 1,而 & 运算结果的最低位是 1 还是 0 完全取决于 hash 值二进制的最低位。如果 length 为奇数,则 length-1 则为偶数,则 length-1 二进制的最低位横为 0,则 & 位运算的结果最低位横为 0,即横为偶数。这样 table 数组就只可能在偶数下标的位置存储了数据,浪费了所有奇数下标的位置,这样也更容易产生 hash 冲突。这也是 HashMap 的容量为什么总是 2 的平方数的原因。我们来用表格对比 length=15 和 length=16 的情况:
h | lenght - 1 | h & (lenght - 1) | 结果 |
---|---|---|---|
0 | 14 | 0000 & 1110 = 0000 | 0 |
1 | 14 | 0001 & 1110 = 0000 | 0 |
2 | 14 | 0010 & 1110 = 0010 | 2 |
3 | 14 | 0011 & 1110 = 0010 | 2 |
... | 14 | ... & 1110 | 偶数 |
h | lenght - 1 | h & (lenght - 1) | 结果 |
0 | 15 | 0000 & 1111 = 0000 | 0 |
1 | 15 | 0001 & 1111 = 0001 | 1 |
2 | 15 | 0010 & 1111 = 0010 | 2 |
3 | 15 | 0011 & 1111 = 0011 | 3 |
... | 15 | ... & 1111 | 可奇可偶 |
注意 : 而 key 的 hash 值,并不仅仅只是 key 对象的 hashCode() 方法的返回值,还会经过扰动函数的扰动,以使 hash 值更加均衡。
因为 hashCode() 是 int 类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。
但就算原本的 hashCode() 取得很好,每个 key 的 hashCode() 不同,但是由于 HashMap 的数组的长度远比 hash 取值范围小,默认是 16,所以当对 hash 值以桶的长度取余,以找到存放该 key 的数组的下标时,由于取余是通过与操作完成的,会忽略 hash 值的高位。因此只有 hashCode() 的低位参加运算,发生不同的 hash 值,但是得到的 index 相同的情况的几率会大大增加,这种情况称之为 hash 碰撞。 即,碰撞率会增大。
我们再回到 put 方法中,我们已经根据 key 得到 hash 值,然后根据 hash 值算出在 table 的存储下标了,接着就是这段 for 代码了:
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
// 先判断hash值是否一样,如果一样,再判断key是否一样
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
首先取出 table 中下标为 i
的 Entry,然后判断该 Entry 的 hash 值和 key 是否和要存储的 hash 值和 key 相同,如果相同,则表示要存储的 key 已经存在于 HashMap,这时候只需要替换已存的 Entry 的 value 值即可。如果不相同,则取 e.next
继续判断,其实就是遍历 table 中下标为 i
的 Entry 单向链表,找是否有相同的 key 已经在 HashMap 中,如果有,就替换 value 为最新的值,所以 HashMap 中只能存储唯一的 key。
关于需要同时比较hash值和key有以下两点需要注意:
为什么比较了hash 值还需要比较 key :因为不同对象的 hash 值可能一样。
为什么不只比较 equal :因为 equal 可能被重写了,重写后的 equal 的效率要低于 hash 的直接比较。
假设我们是第一次 put,则整个 for 循环体都不会执行,我们继续往下看 put 方法。
modCount++;
addEntry(hash, key, value, i);
return null;
这里主要看 addEntry 方法,它应该就是把 key 和 value 封装成 Entry,然后加入到 table 中的实现。来看看它的方法体:
void addEntry(int hash, K key, V value, int bucketIndex) {
// 当前HashMap存储元素的个数大于HashMap扩容的阀值,则进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 使用key、value创建Entry并加入到table中
createEntry(hash, key, value, bucketIndex);
}
这里牵涉到了 HashMap 的扩容,我们先不讨论扩容,后面会讲到。然后调用了createEntry 方法,它的实现如下:
void createEntry(int hash, K key, V value, int bucketIndex) {
// 取出table中下标为bucketIndex的Entry
Entry e = table[bucketIndex];
// 利用key、value来构建新的Entry
// 并且之前存放在table[bucketIndex]处的Entry作为新Entry的next
// 把新创建的Entry放到table[bucketIndex]位置
table[bucketIndex] = new Entry<>(hash, key, value, e);
// HashMap当前存储的元素个数size自增
size++;
}
这里其实就是根据 hash、key、value 以及 table 中下标为 bucketIndex
的 Entry 去构建一个新的 Entry,其中 table 中下标为 bucketIndex
的 Entry 作为新 Entry 的 next,这也说明了,当 hash 冲突时,采用的拉链法来解决 hash 冲突的,并且是把新元素是插入到单边表的表头。如下所示:
2.5 扩容
如果当前 HashMap 中存储的元素个数达到扩容的阀值,且当前要存在的值在 table 中要存放的位置已经有存值时,怎么处理的?我们再来看看 addEntry 方法中的扩容相关代码:
if ((size >= threshold) && (null != table[bucketIndex])) {
// 将table表的长度增加到之前的两倍
resize(2 * table.length);
// 重新计算哈希值
hash = (null != key) ? hash(key) : 0;
// 从新计算新增元素在扩容后的table中应该存放的index
bucketIndex = indexFor(hash, table.length);
}
接下来我们看看 resize 是如何将 table 增加长度的:
void resize(int newCapacity) {
// 保存老的table和老table的长度
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个新的table,长度为之前的两倍
Entry[] newTable = new Entry[newCapacity];
// hash有关
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
// 这里进行异或运算,一般为true
boolean rehash = oldAltHashing ^ useAltHashing;
// 将老table的原有数据,从新存储到新table中
transfer(newTable, rehash);
// 使用新table
table = newTable;
// 扩容后的HashMap的扩容阀门值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
final Node[] resize() {
//oldTab 为当前表的哈希桶
Node[] oldTab = table;
//当前哈希桶的容量 length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//当前的阈值
int oldThr = threshold;
//初始化新的容量和阈值为0
int newCap, newThr = 0;
//如果当前容量大于0
if (oldCap > 0) {
//如果当前容量已经到达上限
if (oldCap >= MAXIMUM_CAPACITY) {
//则设置阈值是2的31次方-1
threshold = Integer.MAX_VALUE;
//同时返回当前的哈希桶,不再扩容
return oldTab;
}//否则新的容量为旧的容量的两倍。
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//如果旧的容量大于等于默认初始容量16
//那么新的阈值也等于旧的阈值的两倍
newThr = oldThr << 1; // double threshold
}//如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;//那么新表的容量就等于旧的阈值
else {}//如果当前表是空的,而且也没有阈值。代表是初始化时没有任何容量/阈值参数的情况 // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//此时新表的容量为默认的容量 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值为默认容量16 * 默认加载因子0.75f = 12
}
if (newThr == 0) {//如果新的阈值是0,对应的是 当前表是空的,但是有阈值的情况
float ft = (float)newCap * loadFactor;//根据新表容量 和 加载因子 求出新的阈值
//进行越界修复
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//根据新的容量 构建新的哈希桶
Node[] newTab = (Node[])new Node[newCap];
//更新哈希桶引用
table = newTab;
//如果以前的哈希桶中有元素
//下面开始将当前哈希桶中的所有节点转移到新的哈希桶中
if (oldTab != null) {
//遍历老的哈希桶
for (int j = 0; j < oldCap; ++j) {
//取出当前的节点 e
Node e;
//如果当前桶中有元素,则将链表赋值给e
if ((e = oldTab[j]) != null) {
//将原哈希桶置空以便GC
oldTab[j] = null;
//如果当前链表中就一个元素,(没有发生哈希碰撞)
if (e.next == null)
//直接将这个元素放置在新的哈希桶里。
//注意这里取下标 是用 哈希值 与 桶的长度-1 。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高
newTab[e.hash & (newCap - 1)] = e;
//如果发生过哈希碰撞 ,而且是节点数超过8个,转化成了红黑树(暂且不谈 避免过于复杂, 后续专门研究一下红黑树)
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
//如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。
else { // preserve order
//因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量
//低位链表的头结点、尾节点
Node loHead = null, loTail = null;
//高位链表的头节点、尾节点
Node hiHead = null, hiTail = null;
Node next;//临时节点 存放e的下一个节点
do {
next = e.next;
//这里又是一个利用位运算 代替常规运算的高效点: 利用哈希值 与 旧的容量,可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位
if ((e.hash & oldCap) == 0) {
//给头尾节点指针赋值
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}//高位也是相同的逻辑
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}//循环直到链表结束
} while ((e = next) != null);
//将低位链表存放在原index处,
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//将高位链表存放在新index处
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
再来看看 transfer 方法是如何将把老 table 的数据,转到扩容后的 table 中的:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历老的table数组
for (Entry e : table) {
// 遍历老table数组中存储每条单项链表
while(null != e) {
// 取出老table中每个Entry
Entry next = e.next;
if (rehash) {
//重新计算hash
e.hash = null == e.key ? 0 : hash(e.key);
}
// 根据hash值,算出老table中的Entry应该在新table中存储的index
int i = indexFor(e.hash, newCapacity);
// 让老table转移的Entry的next指向新table中它应该存储的位置
// 即插入到了新table中index处单链表的表头
e.next = newTable[I];
// 将老table取出的entry,放入到新table中
newTable[i] = e;
// 继续取老talbe的下一个Entry
e = next;
}
}
}
从上面易知,扩容就是先创建一个长度为原来 2 倍的新 table,然后通过遍历的方式,将老 table 的数据,重新计算 hash 并存储到新 table 的适当位置,最后使用新的 table,并重新计算 HashMap 的扩容阀值。
2.6 get 方法
public V get(Object key) {
// 当key为null, 这里不讨论,后面统一讲
if (key == null)
return getForNullKey();
// 根据key得到key对应的Entry
Entry entry = getEntry(key);
//
return null == entry ? null : entry.getValue();
}
然后我们看看 getEntry 是如果通过 key 取到 Entry 的:
final Entry getEntry(Object key) {
// 根据key算出hash
int hash = (key == null) ? 0 : hash(key);
// 先算出hash在table中存储的index,然后遍历table中下标为index的单向链表
for (Entry e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 如果hash和key都相同,则把Entry返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
取值,最简单粗暴的方式肯定是遍历 table,并且遍历 table 中存放的单向链表,这样的话,get 的 时间复杂度 就是 O(n 的平方),但是 HashMap 的 put 本身就是有规律的存储,所以,取值时,可以按照规律去降低时间复杂度。上面的代码比较简单,其实节约的就是遍历 table 的过程,因为我们可以用 key 的 hash 值算出 key 对应的 Entry 所在链表在在 table 的下标。这样,我们只要遍历单向链表就可以了,时间复杂度降低到 O(n)。
get 方法的取值过程如下图所示:
使用 entrySet 取数据
HashMap 除了提供 get 方法,通过 key 来取数据的方式,还提供了 entrySet 方法来遍历 HashMap 的方式取数据。如下:
Map hashMap = new HashMap();
hashMap.put("name1", "josan1");
hashMap.put("name2", "josan2");
hashMap.put("name3", "josan3");
Set> set = hashMap.entrySet();
Iterator> iterator = set.iterator();
while(iterator.hasNext()) {
Entry entry = iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println("key:" + key + ",value:" + value);
}
结果可知,HashMap 存储数据是 无序的。
我们这里主要是讨论,它是如何来完成遍历的。HashMap重写了entrySet。
public Set> entrySet() {
return entrySet0();
}
private Set> entrySet0() {
Set> es = entrySet;
// 相当于返回了new EntrySet
return es != null ? es : (entrySet = new EntrySet());
}
代码比较简单,直接 new EntrySet 对象并返回,EntrySet 是 HashMap 的内部类,注意,不是静态内部类,所以它的对象会默认持有外部类 HashMap 的对象,定义如下:
private final class EntrySet extends AbstractSet> {
// 重写了iterator方法
public Iterator> iterator() {
return newEntryIterator();
}
// 不相关代码
...
}
我们主要是关心 iterator 方法,EntrySet 重写了该方法,所以调用 Set 的 iterator 方法,会调用到这个重写的方法,方法内部很简单单,直接调用了newEntryIterator 方法,返回了一个自定义的迭代器。我们看看 newEntryIterator:
Iterator> newEntryIterator() {
return new EntryIterator();
}
可看到,直接 new 了一个 EntryIterator 对象返回,看看 EntryIterator 的定义:
private final class EntryIterator extends HashIterator> {
// 重写了next方法
public Map.Entry next() {
return nextEntry();
}
}
EntryIterator 是继承了HashIterator,我们再来看看 HashIterator 的定义:
private abstract class HashIterator implements Iterator {
Entry next; // 下一个要返回的Entry
int expectedModCount; // For fast-fail
int index; // 当前table上下标
Entry current; // 当前的Entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Entry nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null);
}
current = e;
return e;
}
// 不相关
......
}
我们先看构造方法:
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
// 这里其实就是遍历table,找到第一个返回的Entry next
// 该值是table数组的第一个有值的Entry,所以也肯定是单向链表的表头
while (index < t.length && (next = t[index++]) == null);
}
}
以上,就是我们调用了 Iterator
接下来就是使用 while(iterator.hasNext()) 去循环判断是否有下一个 Entry,EntryIterator 没有实现 hasNext 方法,所以也是调用的 HashIterator 中的 hasNext,我们来看看该方法:
public final boolean hasNext() {
// 如果下一个返回的Entry不为null,则返回true
return next != null;
}
该方法很简单,就是判断下一个要返回的 Entry next 是否为 null,如果 HashMap 中有元素,那么第一次调用 hasNext 时 next
肯定不为 null,且是 table 数组的第一个有值的 Entry,也就是第一条单向链表的表头 Entry。
接下来,就到了调用 EntryIterator.next
去取下一个 Entry 了, EntryIterator 对 next 方法进行了重写,看看该方法:
public Map.Entry next() {
return nextEntry();
}
直接调用了 nextEntry 方法,返回下一个 Entry,但是 EntryIterator 并没有重写 nextEntry,所以还是调用的 HashIterator 的 nextEntry 方法,方法如下:
final Entry nextEntry() {
// 保存下一个需要返回的Entry,作为返回结果
Entry e = next;
// 如果遍历到table上单向链表的最后一个元素时
if ((next = e.next) == null) {
Entry[] t = table;
// 继续往下寻找table上有元素的下标
// 并且把下一个talbe上有单向链表的表头,作为下一个返回的Entry next
while (index < t.length && (next = t[index++]) == null);
}
current = e;
return e;
}
其实 nextEntry 的主要作用有两点:
- 把当前遍历到的 Entry 返回
- 准备好下一个需要返回的 Entry
如果当前返回的 Entry 不是单向链表的最后一个元素,那只要让下一个返回的 Entrynext 为当前 Entry 的 next 属性(下图红色过程);如果当前返回的 Entry 是单向链表的最后一个元素,那么它就没有 next 属性了,所以要寻找下一个 table 上有单向链表的表头(下图绿色过程)
可知,HashMap的遍历,是先遍历table,然后再遍历table上每一条单向链表,如上述的HashMap遍历出来的顺序就是Entry1、Entry2....Entry6,但显然,这不是插入的顺序,所以说:HashMap是无序的。
2.8 对 key 为 null 的处理
先看 put 方法时,key 为 null:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
//其他不相关代码
.......
}
看看 putForNullKey 的处理:
private V putForNullKey(V value) {
// 遍历table[0]上的单向链表
for (Entry e = table[0]; e != null; e = e.next) {
// 如果有key为null的Entry,则替换该Entry中的value
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 如果没有key为null的Entry,则构造一个hash为0、key为null、value为真实值的Entry
// 插入到table[0]上单向链表的头部
addEntry(0, null, value, 0);
return null;
}
其实 key 为 null 的 put 过程,跟普通 key 值的 put 过程很类似,区别在于 key 为 null 的 hash 为 0,存放在 table[0] 的单向链表上而已。
我们再来看看对于 key为 null 的取值:
public V get(Object key) {
if (key == null)
return getForNullKey();
//不相关的代码
......
}
取值就是通过 getForNullKey 方法来完成的,代码如下:
private V getForNullKey() {
// 遍历table[0]上的单向链表
for (Entry e = table[0]; e != null; e = e.next) {
// 如果key为null,则返回该Entry的value值
if (e.key == null)
return e.value;
}
return null;
}
key 为 null 的取值,跟普通 key 的取值也很类似,只是不需要去算 hash 和确定存储在 table 上的 index 而已,而是直接遍历 talbe[0]。
所以,在 HashMap 中,不允许 key 重复,而 key 为 null 的情况,只允许一个 key 为 null 的 Entry,并且存储在 table[0] 的单向链表上。
2.9 remove 方法
HashMap 提供了 remove 方法,用于根据 key 移除 HashMap 中对应的 Entry:
public V remove(Object key) {
Entry e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
首先调用 removeEntryForKey 方法把 key 对应的 Entry 从 HashMap 中移除。然后把移除的值返回。我们继续看 removeEntryForKey 方法:
final Entry removeEntryForKey(Object key) {
// 算出hash
int hash = (key == null) ? 0 : hash(key);
// 得到在table中的index
int i = indexFor(hash, table.length);
// 当前结点的上一个结点,初始为table[index]上单向链表的头结点
Entry prev = table[I];
Entry e = prev;
while (e != null) {
// 得到下一个结点
Entry next = e.next;
Object k;
// 如果找到了删除的结点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
// 如果是table上的单向链表的头结点,则直接让把该结点的next结点放到头结点
if (prev == e)
table[i] = next;
else
// 如果不是单向链表的头结点,则把上一个结点的next指向本结点的next
prev.next = next;
// 空实现
e.recordRemoval(this);
return e;
}
// 没有找到删除的结点,继续往下找
prev = e;
e = next;
}
return e;
}
其实逻辑也很简单,先根据 key 算出 hash,然后根据 hash 得到在 table 上的 index,再遍历 talbe[index] 的单向链表,这时候需要看要删除的元素是否就是单向链表的表头,如果是,则直接让 table[index] = next,即删除了需要删除的元素;如果不是单向链表的头,那表示有前面的结点,则让 pre.next = next,也删除了需要删除的元素。
2.10 线程安全问题
由前面 HashMap 的 put 和 get 方法分析可得,put 和 get 方法真实操作的都是 Entry[] table 这个数组,而所有操作都没有进行同步处理,所以 HashMap 是线程不安全的。如果想要实现线程安全,推荐使用 ConcurrentHashMap。
3 总结
- HashMap 是基于哈希表实现的,用 Entry[] 来存储数据,而 Entry 中封装了 key、value、hash 以及 Entry 类型的 next
- HashMap 存储数据是无序的
- hash 冲突是通过拉链法解决的
- HashMap 的容量永远为 2 的幂次方,有利于哈希表的散列
- HashMap 不支持存储多个相同的 key,且只保存一个 key 为 null 的值,多个会覆盖
- put 过程,是先通过 key 算出 hash,然后用 hash 算出应该存储在 table 中的 index,然后遍历 table[index],看是否有相同的 key 存在,存在,则更新 value;不存在则插入到 table[index] 单向链表的表头,时间复杂度为 O(n)
- get 过程,通过 key 算出 hash,然后用 hash 算出应该存储在 table 中的 index,然后遍历 table[index],然后比对 key,找到相同的 key,则取出其 value,时间复杂度为 O(n)
- HashMap 是线程不安全的,如果有线程安全需求,推荐使用 ConcurrentHashMap
特别申明
这篇对 HashMap 分析特别清楚的文章,全部来自于
艺旭家 的 图解HashMap原理,用于自己学习,特此记录。
张旭通--面试必备:HashMap源码解析(JDK8)
开始结束的图片来源网络,侵删