HashMap 是双列存储元素,存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。JDK1.7之前 HashMap 由数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(通过算法得到的地址可能一致,然而hash值或者value值不一样,采取多次比较的方法)而存在的(“拉链法”解决冲突).JDK1.8 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树,以减少搜索时间。
JDK1.8解读
HashMap的继承关系图谱
通过图谱可以观察出来HashMap是继承了AbstractMap,进一步对HashMap解读:可以看到HashMap实现了Map、Cloneable 、Serializable接口等。
定义静态常量
构造函数
对构造函数进一步解读
// 无参构造函数
public HashMap() {
// 负载因子为默认值0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 带参构造函数,参数为自定义容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 带参构造函数,参数为自定义容量和自定义加载因子
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或者负载因子为null,抛出负载因子非法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 调用tableSizeFor方法,
this.threshold = tableSizeFor(initialCapacity);
}
// 带参构造函数,参数为map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 调用putMapEntries
putMapEntries(m, false);
}
HashMap数据结构
static class Node<K,V> implements Map.Entry<K,V> {
// 哈希值不可改变
final int hash;
// key值不可以改变
final K key;
V value;
// 下一个节点的引用
Node<K,V> next;
// Node结构存储哈希值,键,值,下一个节点的引用
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;
// 对象实例化一个Entry
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
// 如果对象的键与实例化对象的键相等并且对象键与实例化值相等,返回true
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//节点的父亲
TreeNode<K, V> parent;
//节点的左孩子
TreeNode<K, V> left;
//节点的右孩子
TreeNode<K, V> right;
//节点的前一个节点
TreeNode<K, V> prev;
//true表示红节点,false表示黑节点
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//获得树的根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
hash值
// 传入对象的键值
static final int hash(Object key) {
// 初始化一个变量来存储键的哈希值
int h;
// 如果对象的键为不为null,键的哈希值与运算16得到一个哈希值;这里跟jdk1.7 有非常大的不同
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}数组大小
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,这个的意思就是: 衡量数组是否需要扩增的一个标准。
loadFactor 加载因子
loadFactor加载因子是控制数组存放数据的疏密程度,分布均匀。 loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。通过科学计算,泊松定理,官方给出的是0.75f。 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。因此,与ArrayList一样,应该尽可能去减少触发扩容机制。
添加键值对
put()方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//初始化一个Node数组tab
Node<K,V>[] tab;
//初始化一个节点Node
Node<K,V> p;
int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等;为红黑树结点
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点
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;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) {
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
} putMapEntries方法:
resize()方法
final Node<K,V>[] resize() {
// 将初始定义的数组赋值给oldTable
Node<K,V>[] oldTab = table;
// 判断初始数组是否为null,确定数组容量大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获得原先的临界值
int oldThr = threshold;
int newCap, newThr = 0;
// 如果原先容量大于0
if (oldCap > 0) {
// 如果原先容量大于最大值,把临界值改为最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 新容量大小等于原先容量右移一位,相当于扩大了2倍,新容量要小于最大值,旧容量要大于默认容量大小
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新临界值等于旧临界值右移一位,相当于扩大原来的2倍
newThr = oldThr << 1; // double threshold
}
// 如果原先临界值大于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;
// 新临界值等于默认负载因子*默认初始容量
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;
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
//修改hashMap的table为新建的newTab
table = newTab;
//如果旧table不为空,将旧table中的元素复制到新的table中
if (oldTab != null) {
//遍历旧哈希表的每个桶,将旧哈希表中的桶复制到新的哈希表中
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
//如果旧桶不为null,使用e记录旧桶
if ((e = oldTab[j]) != null) {
//将旧桶置为null
oldTab[j] = null;
//如果旧桶中只有一个node
if (e.next == null)
//将e也就是oldTab[j]放入newTab中e.hash & (newCap - 1)的位置
newTab[e.hash & (newCap - 1)] = e;
//如果旧桶中的结构为红黑树
else if (e instanceof TreeNode)
//将树中的node分离
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else {
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 {// 原索引+oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
//如果桶数组table为空或者桶数组table的长度小于64不符合转化为红黑树的条件
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//调用扩容
resize();
//如果符合转化为红黑树的条件,而且hash对应的桶不为null
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 红黑树的头、尾节点
TreeNode<K, V> hd = null, tl = null;
//遍历链表
do {
//替换链表node为树node,建立双向链表
TreeNode<K, V> p = replacementTreeNode(e, null);
// 确定树头节点
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//遍历链表插入每个节点到红黑树
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
添加元素思路
首先,调用newKey所在类的hashCode()计算newKey哈希值,通过算法得到在node数组中的存放位置;
如果此位置不存在数据,就直接将键值对插入到数组中;
如果此位置已经存在数据,则表明该位置可以存在一个或者多个数据(链表或者是红黑树),依次比较newKey与多数据的哈希值:
如果newKey的哈希值与已经存在的数据哈希值不一样,添加到链表的末尾,注意会添加到红黑树中;
如果newKey的哈希值与已经存在的数据哈希值一样,接着比较,调用newKey所在类的equals():
如果返回是false,添加节点;
如果返回true,替换原来的value。
最后需要注意的是:在添加过程中涉及到扩容问题,默认扩容方式为扩容为原先容量的2倍,将所有的数据复制过来,重新计算hash,找到对应的位置存放。get()方法
public V get(Object key) {
Node<K, V> e;
//传入key,调用hash方法得到hash值查询node节点,判断节点是否存在,存在则返回该节点的value值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K, V> getNode(int hash, Object key) {
Node<K, V>[] tab;
Node<K, V> first, e;
int n;
K k;
//如果哈希表不为空,而且key对应的桶上数据不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果桶中的第一个节点就和指定参数hash和key匹配上了
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//返回桶中的第一个节点
return first;
//如果桶中的第一个节点没有匹配上,而且有后续节点
if ((e = first.next) != null) {
//如果当前的桶采用红黑树,则调用红黑树的get方法去获取节点
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
//如果当前的桶不采用红黑树,即桶中节点结构为链式结构
do {
//遍历链表,直到key匹配
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果哈希表为空,或者没有找到节点,返回null
return null;
}
//得到树节点
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}以上是对1.8版本的几个方法解读,有兴趣对其他方法可以参考文末给出的两篇参考文章。
JDK1.7解读
全局变量
构造函数:
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);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
} JDK 1.8 的无参构造函数相比于 JDK 1.7 无参构造函数,并没有一开始就创建一个长度为16的数组,而是在调用put()方法才新建数组。
数据结构
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
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 Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
void recordAccess(HashMap<K,V> m) {
}
void recordRemoval(HashMap<K,V> m) {
}
} JDK 1.8 的 数据结构相比于 JDK 1.7 数据结构用Node数组代替了Entry数组,添加了treeNode,底层增加了红黑树,改善因链表过长,导致查询效率低下的问题。
hash值
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,减少了扰动。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
put()方法
//定义一个空的Entry数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
public V put(K key, V value) {
//如果数组为空则调用inflateTable()方法
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果传入的key是null,调用putForNullKey()方法,把值传入
if (key == null)
return putForNullKey(value);
//如果key不是null,调用hash()得到哈希值
int hash = hash(key);
//调用indexFor()方法,传入哈希值,与数组的长度,得到该值在数组中的索引位置
int i = indexFor(hash, table.length);
//对数组进行遍历,得到每一个entry
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果entry的哈希值并且键或者值一致,则将原来的值取出来,把当前值赋值进去,调用recordAccess()方法
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 () 添加一个entry 参数是哈希值,键,值,和索引
addEntry(hash, key, value, i);
return null;
}
//调用私有方法
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//空数组调用私有方法tosize
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//得到容量
int capacity = roundUpToPowerOf2(toSize);
//数组临界值 在数组容量乘负载因子与最大容量+1相比取最小
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化entry数组大小
table = new Entry[capacity];
//初始化容量大小
initHashSeedAsNeeded(capacity);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果size大于临界值并且添加entry计算出来的数组索引的值不为null
if ((size >= threshold) && (null != table[bucketIndex])) {
//调用扩容方法,参数是2*数组长度
resize(2 * table.length);
//key不等于null ,计算出key的哈希值
hash = (null != key) ? hash(key) : 0;
//得到在数组中位置
bucketIndex = indexFor(hash, table.length);
}
//创建一个entry
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++;
} 对比JDK1.8put方法
如果定位到的数组位置没有元素 就直接插入;
如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的key比较;
如果key的哈希值相同且值一样就直接覆盖,不同就采用头插法插入元素。resize()方法
//扩容机制
void resize(int newCapacity) {
//将数组赋值给一个
Entry[] oldTable = table;
//旧数组的长度
int oldCapacity = oldTable.length;
//如果旧数组的常长度等于最大容量,临界值为Integer.MAX_VALUE
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个新数组,容量为原来数组的2倍
Entry[] newTable = new Entry[newCapacity];
//将原来数组中的数据复制到新数组
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//得到一个新数组
table = newTable;
//临界值为新数组的容量*负载因子与最大容量+1相比取最小的数
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;
}
}
} get()方法
public V get(Object key) {
//如果键为null,调用getForNullKey
if (key == null)
return getForNullKey();
//得到一个entry
Entry<K,V> entry = getEntry(key);
//如果entry不为null,返回entry的值
return null == entry ? null : entry.getValue();
}