【Java集合】-- HashMap源码解析
文章目录
讲HashMap就不得不说到hash算法
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在記憶體儲存位置的数据结 构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。
源码解析
1.构造方法
无参构造方法
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
loadFactor 是其负载因子,DEFAULT_LOAD_FACTOR默认值0.75f。该值用来判断当hashmap中以使用空间达到该占比是进行扩容。如默认空间大小为16.16*0.75=12.所以当hashmap内的数组有12个空间被使用时就开始扩容resize();
int型参数的构造方法
public HashMap(int initialCapacity) {
//内部调用了传两个参数的构造方法。默认负载因子0.75f
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
int,float两个参数的构造方法
static final int MAXIMUM_CAPACITY = 1 << 30;//2^30
public HashMap(int initialCapacity, float loadFactor) {
//判断传入的容量是否小于0
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);
}
//该方法计算出的值为:2^次方>=initialCapacity
static final int tableSizeFor(int cap) {
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(Map<? extends K, ? extends V> m) {
//设置默认的负载因子
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//传入集合的大小
int s = m.size();
//判断集合中是否有元素
if (s > 0) {
//判断transient Node<K,V>[] table;是否是null
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
//设置其大小
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
//t大于下一次要扩容的大小时,改变其值
threshold = tableSizeFor(t);
}
else if (s > threshold)
//对象中table不为null且传入集合大小大于下一次扩容大小时,进行扩容
resize();
//把传入集合的内容存入当前集合
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//调用了hash函数
putVal(hash(key), key, value, false, evict);
}
}
}
hsah方法
static final int hash(Object key) {
int h;
//对象的hashcode值^(异或)其hashcode的高16位的值
//目的:提高hashcode的随机性,减少hash冲突
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扰动函数
2.添加元素(put()方法)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//返回值为当前key的上一个vlaue,如果没有则为null
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断table数组为0或null时,
if ((tab = table) == null || (n = tab.length) == 0)
//进行初始化
n = (tab = resize()).length;
//判断key是否已经存在于数组中,即是否发生hash冲突
//(n - 1) & hash该计算对应位置的方法是
//n为数组长度,为2的次方如16=00010000
//n-1为00001111
//而无论hash值为多少。如11110010
//(n - 1) & hash计算出的结果都小于n
//所以这便是数组长度为什么始终是2的次幂,保证了不会越界
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指向当前元素
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 {
for (int binCount = 0; ; ++binCount) {
//把要插入元素存放在队尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链表长度大于等于8则对链表进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//链表中存在指定元素,则break
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;
// 在节点被访问后,做点什么事,hashMap中该方法并没有被实现
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果键值对个数大于阈值,则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
该添加元素的过程是:先判断是否存在hash冲突,不存在则直接把元素存入hashMap。hash冲突时,再判断冲突位置的key是否与要存入的key相同,相同就替换旧值。不相同新建一个节点存入红黑树中或挂在链表尾部。
3.扩容方法(resize()方法)
源码:
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
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;
}
//如果旧数组长度的2倍小于最大容量,且旧容量大于默认初始容量就把容量与阈值同时扩大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//使用非默认方法创建map时,第一次插入会走这里
//如果旧容量为0且旧阈值大于0,则把新容量赋值为旧阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//使用默认方法创建map时,第一次插入会走这里。来初始化hashmap的内置数组
else { // zero initial threshold signifies using defaults
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
table = newTab;
//对已存在数组扩容时,从旧数组向新数组转移元素
if (oldTab != null) {
//遍历旧数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//判断j位置是否为空,为空不处理
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//判断该节点是否有后继节点,无后继节点则直接把旧数组j位置的数放在新数组对应位置
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;
}
}
}
}
}
return newTab;
}
扩容方法被调用有两种情况,一是进行hashmap中的数组初始化时,二是真正进行扩容时。初始化没什么好讲的。不过扩容时把数组长度,和阈值扩展为原来的二倍。创建新数组后把旧数组中的元素往旧数组中移动。单节点直接移动到新数组对应位置。如果是一个红黑树则把树进行拆分。如果是链表则也对链表进行拆分。
4.获取元素(get()方法)
源码:
public V get(Object key) {
Node<K,V> e;
//判断对应key的value是否存在,
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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) {
//第一个节点就是我们要取得值,则直接返回
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;
}
5.移除元素(remove())
源码:
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;
//判断table数组存在且key对应位置存在节点
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))))
//如果对应位置的key和要移除的相同,则直接把p赋值node
node = p;
else if ((e = p.next) != null) {
//如果p是树节点,在树中查找key
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//在链表中查找key
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//要删除的节点存在
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//把节点从树中删除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//节点是单节点或链表的首部则把该节点后继节点赋值到节点所在位置
else if (node == p)
tab[index] = node.next;
else
//节点是链表中间结点。此时p为该节点的前一个节点,所以p.next = //node.next;来删除节点
p.next = node.next;
++modCount;
--size;
//留给LinkedList使用
afterNodeRemoval(node);
return node;
}
}
return null;
}
6.树化(treeifyBin())
源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//判断table数组不存在,或者数组长度没有达到最小树化长度时进行扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//对应位置链表存在,开始扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//将普通节点转化为树节点
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);
}
}
关于HashMap常见的问题
1.为什么容量始终是2的幂次?
- 因为计算key所在的桶是 (hash & n-1),如果n是2的幂次,则保证了key所在桶的范围是0 <= index <= n-1。
- 在进行扩容时,原链表会分化为两条链表,高位的位置时 旧容量+从前的位置
2.加载因子为什么是0.75?
假设loadfactory = 1,则键值对个数达到数组容量时,进行扩容,能够极大的利用空间,但是查询慢。假设loadfactory = 0.5,则键值对达到数组容量一半时,进行扩容,查询快,但是利用的空间较少。而0.75则是为了再空间与时间取一个平衡。
3.既然红黑树那么好,为啥hashmap不直接采用红黑树,而是当大于等于8个的时候才转换红黑树?
根据泊松分布的概率学统计,当key所在的桶的链表长度增加,那么新的key到这个桶的概率在不断降低。当链表8长度为时,下一个键值对到这个链表的概率接近于0,所以产生红黑树的概率也不高。
4.JDK1.7 扩容死锁产生原因
因为jdk1.7的扩容方法扩容时
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];
transfer(newTable,initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactory,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.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
}
在多线程的情况下,可能会造成循环引用
5.JDK1.8 为什么不会形成环,如果做到无需rehash?
通过两组指针,将原链表直接截断分为两组高低位链表,避免了向1.7那样节点间相互翻转,形成环,同时也不需要rehash。
6.modCount的作用
modCount是用来记录HashMap中数组被修改的次数。在迭代器迭代时会比较modCount与迭代器对象自身记录的修改次数。如果modCount发生变化会发生并发修改异常。所以如果在迭代的过程中想要删除元素,使用迭代器自带的删除方法。
7.为什么用红黑树而不是其他数结构
红黑树牺牲了一些查找性能 但其本身并不是完全平衡的二叉树。因此插入删除操作效率略高于AVL树,AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些。
8.HashMap 和 HashTable 的区别
- 线程安全性:HashMap 是线程不安全的,而HashTable 是线程安全的,大部分方法都是由 synchronized修饰。
- 效率:HashMap 由于不是线程安全的,所以就单线程环境下,效率由于 HashTable
- 容量:HashMap 默认初始容量为 16,而 HashTable 模式初始容量为 11。如果指定初始容量的话,HashMap 为第一个大于等于 2 的整数次幂的值,而 HashTable 则为指定值。
- 扩容:HashMap 每次扩容容量为原来的 2 倍,而HashTable 为 2n + 1
- 底层数据结构:HashMap 1.7 后,采用数组+链表+红黑树的数据结构,而HashTable并没有采用红黑树的数据结构。