Java集合之HashMap源码剖析(jdk1.8)
文章目录
1、简介
HashMap是较为常用的一种集合,底层结构较为复杂。
由数组+链表+红黑树组成。
HashMap使用key计算出哈希值,将value存放到数组中,当出现哈希冲突时,将冲突的元素通过链表放到数组元素后,当链表或数组中元素数量过多时,转换为红黑树。
数组的查询效率为O(1),链表的查询效率是O(k),红黑树的查询效率是O(log k),所以当元素数量非常多的时候,转化为红黑树能极大地提高效率。
其继承关系如下:
2、源码分析
1、属性
属性主要由几个容量、扩容或者变化的阈值组成。
装载因子用于处理扩容问题。
//初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//数组,也称为桶
transient Node<K,V>[] table;
//链表节点数量进化阈值,达到8进化成红黑树,进化条件①,和条件②相互约束
static final int TREEIFY_THRESHOLD = 8;
//红黑树退化阈值,降到6退化成链表
static final int UNTREEIFY_THRESHOLD = 6;
//整个容器容量达到64才允许某个链表进化成红黑树,进化条件②
static final int MIN_TREEIFY_CAPACITY = 64;
//entrySet方法返回的结果
transient Set<Map.Entry<K,V>> entrySet;
//桶的扩容门槛,容器中元素达到多少时进行扩容:默认构造方法中为capacity * load factor
int threshold;
//装载因子
final float loadFactor;
2、内部类
HashMap中定义的内部类较多。
主要由两个节点类,几个元素集,N个自己的迭代器组成。
//链表的节点类,单向引用
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;
}
//方法略去
}
//内部定义的红黑树节点、多个应用以及一个标记
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;
//方法略去
}
//元素的key组成的Set类
final class KeySet extends AbstractSet<K> {
//内容略去
}
//元素的value组成的Collection类
final class Values extends AbstractCollection<V> {
//内容略去
}
//元素的key-value组成的EntrySet类
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
//内容略去
}
//内部定义的各种迭代器类
abstract class HashIterator
final class KeyIterator extends HashIterator implements Iterator<K>
final class ValueIterator extends HashIterator implements Iterator<V>
final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>>
static class HashMapSpliterator<K,V>
//.......
3、构造方法
HashMap有四个构造方法。
复杂的是这个构造方法:传入容量和装载因子。
/**
检验容量和装载因子是否合法,合法则计算扩容容量。
扩容门槛为传入的初始容量往上取最近的2的n次方
第一次put元素时会发现初始容量就是这次的扩容门槛
**/
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;
//获取扩容门槛
this.threshold = tableSizeFor(initialCapacity);
//这种构造方法得到的容器的初始容量就是扩容门槛,实际操作在resize方法中
}
// 扩容门槛为传入的初始容量往上取最近的2的n次方
static final int tableSizeFor(int cap) {
//减一操作是为了或运算,避免最后得出的结果是预料结果的2倍
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
三个较为简单。
//传入容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//默认
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//传入一个map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
4、核心方法
1、put方法
通过put方法添加元素。
//put方法调用的putVal方法
public V put(K key, V value) {
//hash方法
return putVal(hash(key), key, value, false, true);
}
//hash方法通过key获取哈希值
static final int hash(Object key) {
int h;
//调用key的hashCode方法并且进行^运算取哈希值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/*
五个参数:hash值、key、value。
第四个参数是如果key相同时,是否替换掉原value,ture不替换,false替换掉
第五个参数是表(数组)的创建模式,在此用不上。
返回值V,返回的是旧的value,没有则返回null.此处的旧value参考第四个参数
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
// 如果桶的数量为0,则初始化
if ((tab = table) == null || (n = tab.length) == 0)
// 调用resize()初始化
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;
// 如果桶中第一个元素的key与待插入元素的key相同,保存到e中用于后续修改value值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
// 如果第一个元素是树节点,则调用树节点的putTreeVal插入元素
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历这个桶对应的链表,binCount用于存储链表中元素的个数
for (int binCount = 0; ; ++binCount) {
// 如果链表遍历完了都没有找到相同key的元素,说明该key对应的元素不存在,则在链表最后插入一个新节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果插入新节点后链表长度大于8,则判断是否需要树化,因为第一个元素没有加到binCount中,所以这里-1
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果待插入的key在链表中找到了,则退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果找到了对应key的元素
if (e != null) { // existing mapping for key
// 记录下旧值
V oldValue = e.value;
// 判断是否需要替换旧值
if (!onlyIfAbsent || oldValue == null)
// 替换旧值为新值
e.value = value;
// 在节点被访问后做点什么,在LinkedHashMap中内部调整顺序时用到,此处无用
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 到这里了说明没有找到元素
// 修改次数加1
++modCount;
// 元素数量加1,判断是否需要扩容
if (++size > threshold)
// 扩容
resize();
//在节点插入后做点什么,在LinkedHashMap中内部调整顺序时用到,此处无用
afterNodeInsertion(evict);
// 没找到元素返回null
return null;
}
2、resize方法
扩容方法。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//老容量和老扩容阈值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
//这两个是新容量和新扩容阈值
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
//老容量已经达到最大容量值则不再扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
/*
将新容量扩容为老容量的两倍,此时如果新容量小于最大容量值,
且老容量大于等于默认的初始容量:扩容门槛也加倍
*/
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
//这里是非默认构造方法得到的容器,第一次put元素时到达的地方
//这里新容量为扩容门槛
newCap = oldThr;
else {
//这里是默认构造方法得到的容器,第一次put元素时到达的地方
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];
table = newTab;
//如果原数组中有元素,将其搬到新数组中
if (oldTab != null) {
// 遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
// 如果桶中第一个元素不为空,赋值给e
if ((e = oldTab[j]) != null) {
// 清空旧桶,便于GC回收
oldTab[j] = null;
// 如果这个桶中只有一个元素,则计算它在新桶中的位置并把它搬移到新桶中
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;
// (e.hash & oldCap) == 0的元素放在低位链表中
// 比如,3 & 4 == 0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
// (e.hash & oldCap) != 0的元素放在高位链表中
// 比如,7 & 4 != 0
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历完成分化成两个链表了
// 低位链表在新桶中的位置与旧桶一样(即3和11还在三号桶中)
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位链表在新桶中的位置正好是原来的位置加上旧容量(即7和15搬移到七号桶了)
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3、get方法
通过key获取value
public V get(Object key) {
Node<K,V> e;
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;
// 如果桶的数量大于0并且待查找的key所在的桶的第一个元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查第一个元素是不是要查的元素,如果是直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 如果第一个元素是树节点,则按树的方式查找
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
// 否则就遍历整个链表查找该元素
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
4、remove方法
通过key删除节点。
public V remove(Object key) {
Node<K, V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K, V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K, V>[] tab;
Node<K, V> p;
int n, index;
// 如果桶的数量大于0且待删除的元素所在的桶的第一个元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e;
K k;
V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果第一个元素正好就是要找的元素,赋值给node变量后续删除使用
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 如果第一个元素是树节点,则以树的方式查找节点
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
else {
// 否则遍历整个链表查找元素
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果找到了元素,则看参数是否需要匹配value值,如果不需要匹配直接删除,如果需要匹配则看value值是否与传入的value相等
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// 如果是树节点,调用树的删除方法(以node调用的,是删除自己)
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
else if (node == p)
// 如果待删除的元素是第一个元素,则把第二个元素移到第一的位置
tab[index] = node.next;
else
// 否则删除node节点
p.next = node.next;
++modCount;
--size;
// 删除节点后置处理
afterNodeRemoval(node);
return node;
}
}
return null;
}
5、replace方法
通过key更新value。
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
6、其他方法
其他的一些处理Key和Value集合的方法。可以处理遍历问题。
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
3、总结
(1)HashMap底层基于数组+链表+红黑树实现,当某个桶中元素超过8且总元素数量超过64时,该桶中元素由链表进化成红黑树。
(2)默认构造方法得到的map容器,其扩容门槛是cap*loadfactor;非默认构造方法得到的容器,初始扩容门槛等于初始容量(此处初始容量等于传入的容量往上取最近的2的n次方)。
(3)HashMap允许Null值和Null键。
(4)线程不安全,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap,或者直接使用并发包下的ConcurrentHashMap.
(5)HashMap没有迭代器,但是可以通过KeySet等方法获取对应的Set,从而遍历输出。