简介:
HashMap作为Map的主要实现类;线程不安全的,但是效率高。可以存储null的key和value
底层存储方式
HashMap底层原理在JDk1.7以前和JDK1.8以后有着较大的区别;在JDK1.7之前使用的是数组+链表的存储方式,而在JDK1.8以后采用的是数组+链表+红黑树的存储方式。
对HashMap更加深入的了解
为了更加深入的了解HashMap,我们需要了解到其他存储方式的优缺点与HashMap存储方式的优优点;
引入HashMap
数组在定义的时候,需要指定明确的初始容量。它在堆中本质上是一块连续的内存,所以我们可以通过下标快速的地位。
但是一旦数组被定义后,想要安全的对数组进行扩容,就需要重新定义一个更大的数组,将原数组的元素拷贝进去。这样对我们的操作是非常不方便的,所以就需要用到Java集合类了。
ArrayList底层使用了数组,封装了对数组的各种操作,使我们可以更加轻松的操作数据,但是它虽然查询速度快,但进行插入、删除的时候速度就很慢了,这个时候就需要使用LinkedList了。
LinkedList底层使用了链表,对于插入、删除等操作的速度非常快,但是对于查询的速度就很慢。
对于ArrayList 和 LinkedList,还有 Vector它们都有一些缺点,要么插入删除速度慢、要么就是遍历速度慢。那么有没有一种插入、删除、遍历速度都比较不错的集合类呢?于是 HashMap 就出现了。
HashMap的特点
- 为了实现快速查找,HashMap 选择了数组而不是链表。以利用数组的索引实现 O(1) 复杂度的查找效率。
- 为了利用索引查找,HashMap 引入 Hash 算法, 将 key 映射成数组下标: key -> Index。
- 引入 Hash 算法又导致了 Hash 冲突。为了解决 Hash 冲突,HashMap 采用链地址法,在冲突位置转为使用链表存储。
- 链表存储过多的节点又导致了在链表上节点的查找性能的恶化。为了优化查找性能,HashMap 在链表长度超过 8 之后转而将链表转变成红黑树,以将 O(n) 复杂度的查找效率提升至 O(log n)。
HashMap的结构图
JDK1.7
JDK1.8
JDK1.7的HashMap源码解析
成员变量/类变量
// 数组默认初始容量为16(必须为2的幂次)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 数组最大容量为1 << 30(约为1亿)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当table(Entry数组)未实例化时为空的
static final Entry<?,?>[] EMPTY_TABLE = {};
// table为空数组,这个和上面的可以合并在一起
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// HashMap中集合大小
transient int size;
// 阈值,在数组中超过这个数时进行扩容
int threshold;
// 负载因子
final float loadFactor;
// 修改次数,在添加或删除时+1
transient int modCount;
Entry类
class Entry<K, V> implements MyMap.Entry<K, V> {
// key
private K key;
// value
private V value;
// 指向下一个Entry对象
private Entry<K, V> next;
// 存放每个Entry对象的hash值
private int hash;
public Entry(int hash, K key, V value, Entry<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
this.hash = hash;
}
public Entry(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
return this.value = value;
}
@Override
public String toString() {
return "Entry{" +
"key=" + key +
", value=" + value +
'}';
}
}
构造函数
**HashMap()**函数
// 在使用无参构造函数创建HashMap时调用其有参构造函数
public HashMap() {
// 默认初始化容量为16 默认加载因子为0.75
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
**HashMap(int initialCapacity, float loadFactor)**函数
public HashMap(int initialCapacity, float loadFactor) {
// 如果容器初始化容量小于0,则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果初始容量 > MAXIMUM_CAPACITY(越1亿),令初始容量值=最大容器容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果加载因子 <= 0 抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 给容器加载因子赋值
this.loadFactor = loadFact0or;
// 给容器阈值赋值为初始容量大小
threshold = initialCapacity;
// 只是定义一个函数,可以被继承类重载这个方法
init();
}
HashMap初始化完成,此时HashMap并没有初始化底层数组,只有当第一次添加元素时才会初始化底层数组。
向HanshMap中添加key,value
**put(K key, V value)**函数
public V put(K key, V value) {
// 如果table为空的话,初始化数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果key为空的化,调用putForNullKey(value)将其添加到table[0]上,如果table[0]不为空,则添加它的下一个节点上。
if (key == null)
return putForNullKey(value);
// 计算key的hash值
int hash = hash(key);
// 根据hash值和数组长度计算它在数组中的下标
int i = indexFor(hash, table.length);
// 遍历下标为i的链表,直到e为空
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 判断hash值与key值是否与链表中的相等,相等的化就覆盖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++;
addEntry(hash, key, value, i);
return null;
}
**inflateTable(int toSize)**函数
private void inflateTable(int toSize) {
// 获取数组容量大小为16
int capacity = roundUpToPowerOf2(toSize);
// HashMap阈值为 capacity * loadFactor = 12.
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化数组,长度为16
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
**addEntry(int hash, K key, V value, int bucketIndex)**函数
向容器中添加key-value键值对
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果满足以下条件,则对数组进行扩容,并且重新计算key在数组中的下标,将其重新存入数组中
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);
}
**resize(int newCapacity)**函数
对HashMap集合进行扩容
void resize(int newCapacity) {
// 获取旧的数组
Entry[] oldTable = table;
// 获取旧的容量
int oldCapacity = oldTable.length;
// 如果旧的容量等于最大容量(约1亿),则阈值等于Integer的最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建容量为新容量的数组
Entry[] newTable = new Entry[newCapacity];
// 移动所有数据从旧数组到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 令HashMap中的table等于newTable。
table = newTable;
// 阈值为newCapacity * loadFactor
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
**createEntry(int hash, K key, V value, int 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);
// hashMap大小+1
size++;
}
通过key获取HashMap中的value
**V get(Object key)**函数
public V get(Object key) {
// 如果key为空,则去table[0]的链表中查找
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
// 如果entry为空,则返回空,否则返回entry.getValue()
return null == entry ? null : entry.getValue();
}
getEntry(Object key) 函数
final Entry<K,V> getEntry(Object key) {
// 如果集合中大小为0,则直接返回空
if (size == 0) {
return null;
}
// 计算key的hash值
int hash = (key == null) ? 0 : hash(key);
// 使用key的hash值计算数组下标,并在对应的链表中查询
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 满足以下条件则返回value,否则继续遍历链表
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
JDK1.7中存在的问题
在JDK1.7版本使用 链表头插赋值法,在多线程的情况下会导致一个死循环问题。在JDK1.8的时候已经解决该问题
主要看这个函数:
**transfer(Entry[] newTable, boolean rehash)**函数
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
// 增强for循环遍历数组
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 = 新数组中下标为i的链表,如果为空,则下标不冲突。
e.next = newTable[i];
// 使用头插法插入链表中,所以原来的链表会反过来
newTable[i] = e;
// 使用next遍历链表
e = next;
}
}
}
B.next = A;
A.next = B;
// 产生了闭环
原理分析:因为每次数组在扩容的时候,新的数组长度发生了变化,需要从新去计算index值,需要将原来的table中的数据移动到新的table中;在HashMap1.7版本中的598行代码e.next = newTable[i];直接改变原来的table的next关系。
如果在多线程中,T1线程修改了链表中的结构,而T2线程同时对这个链表进行操作时,就会发生很大的问题。
根本问题是在复制数据的过程中,修改了原本链表的结构,使原本的链表首位相连导致出现脏读的数据。
解决办法:使用ConcurrentHashMap代替
误区:扩容的时候不是重新计算hash值,而是重新计算index,hash值保存在entry中。
JDK1.8源码解析
在了解JDK1.8之前,推荐先去学习红黑树。
成员变量/类变量
// 默认的初始化容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大的容量(约为1亿)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
static final int MIN_TREEIFY_CAPACITY = 64;
// table数组,用来存储key-value键值对的数组
transient Node<K,V>[] table;
// 集合大小
transient int size;
// 修改次数
transient int modCount;
// 数组扩容的阈值
int threshold;
// 负载因子
final float loadFactor;
Node类
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
构造函数
无参构造函数
1、HashMap函数
public HashMap() {
// 负载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
有参构造函数
**1、HashMap(Map<? extends K, ? extends V> m)**函数
public HashMap(Map<? extends K, ? extends V> m) {
// 加载因子为0.75
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 将map集合中的数据存入到新创建的hashMap对象中
putMapEntries(m, false);
}
**putMapEntries(Map<? extends K, ? extends V> m, boolean evict)**函数
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// s = 集合map的大小
int s = m.size();
// 如果m中存储的元素不为0
if (s > 0) {
// 判断table是否已经初始化,如果没有的话则对threshold初始化
if (table == null) { // pre-size
// 根据需要阈值计算要创建的HashMap的容量
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
// 把要创建的HashMap的容量存储在threshold中
threshold = tableSizeFor(t);
}
// 如果table不为空,说明已经被初始化,
// 判断需要插入的数据大小是否大于threshold,如果是的话,就扩容。
else if (s > threshold)
resize();
// 遍历m集合
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
// 获取key、value
K key = e.getKey();
V value = e.getValue();
// 调用putVal函数将其插入map中
putVal(hash(key), key, value, false, evict);
}
}
}
向集合中添加元素
**put(K key, V value)**函数
public V put(K key, V value) {
// 调用putVal函数进行添加操作,此时底层数组还未创建
return putVal(hash(key), key, value, false, true);
}
**putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)**函数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab就是数组中存放的所有的链表
Node<K,V>[] tab;
// p单个链表的某一个节点
Node<K,V> p;
// n就是当前数组的长度
int n, i;
// 如果当前我们的hashMap中的table中的数组为空的情况下,就需要进行扩容。(数组初始化)
if ((tab = table) == null || (n = tab.length) == 0)
// tab = table,此时tab指向table的地址
// n为扩容之后tab的长度。
n = (tab = resize()).length;
// i就是当前key计算hash值存放在数组中的下标
// 如果p为空,说明可以将key-value直接存入数组中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//否则key可能产生了下标冲突
else {
Node<K,V> e; K k;
// 如果p的hash相等,key也相等,则对key的value进行覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 令e指向p的地址
e = p;
// 否则如果p的类型为红黑树节点,如果是红黑树节点,就以红黑树的方式存放
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// binCount 作用可以进行循环
for (int binCount = 0; ; ++binCount) {
// 如果没有下一个节点
if ((e = p.next) == null) {
// 则链表的下一个节点为当前key-value 尾插法
p.next = newNode(hash, key, value, null);
// 如果binCount达到阈值,则转为红黑树存储数据
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果hash值相等,key也相等情况下,实现对我们value的覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// p指向下一个节点
p = e;
}
}
// 判断当前e是否为空
if (e != null) { // existing mapping for key
// 否则对其value进行覆盖
V oldValue = e.value;
// onlyIfAbsent:覆盖,如果为true,则不更改现有值,默认为false:更改现有值。
// 如果原值为空,则修改value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 空的,给子类实现的方法
afterNodeAccess(e);
// 返回旧的数据
return oldValue;
}
}
// 修改次数+1
++modCount;
// 如果size > threshold,则对数组进行扩容
if (++size > threshold)
resize();
// 空的,给子类实现的方法
afterNodeInsertion(evict);
return null;
}
**resize()**函数
对数组进行扩容操作(resize函数也包含对table的初始化操作)
final Node<K,V>[] resize() {
// 获取原来table的数组
Node<K,V>[] oldTab = table;
// 如果原数组的长度为空的情况下,数组的长度为0.否则获取数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取原来的阈值容量大小
int oldThr = threshold;
// newCap:新的容量大小,newThr:新的阈值容量大小
int newCap, newThr = 0;
// 如果原来的大小为0的情况下
if (oldCap > 0) {
// 如果原来的容量大于最大容量限制的情况下
if (oldCap >= MAXIMUM_CAPACITY) {
// 阈值容量 = Integer的最大值
threshold = Integer.MAX_VALUE;
// 返回旧的tab
return oldTab;
}
// 新的容量 = 旧的容量 * 2
// 并且新的容量 >= 默认的容量(16)且小于最大容量限制
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的阈值容量 = 原来的阈值 * 2
newThr = oldThr << 1; // double threshold
}
// 否则如果旧的容量 = 0,旧的阈值 > 0
else if (oldThr > 0) // initial capacity was placed in threshold
// 新的容量 = 旧的阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 新的容量为16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的阈值为 16 * 0.75 这是对数组进行初始化
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阈值容量等于0
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 对当前对象的变量进行赋值
threshold = newThr;
// 新的table容量 = 16
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 给table进行赋值
table = newTab;
// 如果旧的tab不为空,则将原数组中的数据复制到newTab中
-------------------------------------------------------
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// e.hash & (newCap - 1)为计算数据在新的tab上的下标
// 如果e的下一个节点为空,则直接复制到新的tab上
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回newTab
return newTab;
}
上述中分界线后的代码即是将数据从旧tab复制到新tab中的操作。
在这个操作过程中,创建了两个新的链表:lo链表和li链表;
链表的拆分
在遍历第j个链表的过程中,按照当前的节点是否满足(e.hash & oldCap) == 0的条件将节点添加到lo链表(满足条件)和li链表(不满足条件)。
最后会将lo链表添加到newTab的j位置上,将li链表添加到newTab的j+oldCap位置上。
从集合中获取key的value值
**get(Object key)**函数
public V get(Object key) {
Node<K,V> e;
// 计算key的hash值,通过hash值和key获取node节点,如果为空返回空,否则返回node的value值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
**getNode(int hash, Object key)**函数
final Node<K,V> getNode(int hash, Object key) {
// 判断table数组是否为空、tab的长度是否大于0、根据hash值计算对应下标,获取对于节点。
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) {
// 如果first的hash值与hash值相等且first的key与key相等,则直接返回当前节点。
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果first的下一个节点不为空
if ((e = first.next) != null) {
// 如果first节点为树形节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 遍历链表知道e的下一个节点为空
// 如果满足以下条件则返回e节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}