这就是HashMap
java中有3大集合,Map,Set,List 是我们开发必须要掌握的点。你可以花3-5分钟的时间看完这篇文章,我会从源码的角度分析Map中最常用的HashMap(java1.8)。无论您是开发了很多年经验的前辈,还是和我一样刚出来工作的小白,这篇都是不可不看的文章。
相关数据结构
-ArraryList 用的特别的多,相信很多人都知道它是一个数组的结构,但是本人不太喜欢听别人说的,我喜欢自己去研究,所以我去特地看了一下源码,我觉得其中它的add
方法一眼就能看出来它是一个数组的结构
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 把数组的长度+1
elementData[size++] = e; //赋值
return true;
}
附带上图解:
-LinkedList 也是用的特别多的list,那也有很多人知道他是一个双向链表的结构,那也挑选了一段能很快证明的源码出来
final E element = x.item; //当前的元素
final Node<E> next = x.next; //指向下一个节点
final Node<E> prev = x.prev; //只想上一个节点
附带上图解:
大胆的推断一下
HashMap=数组+链表
那首先我们要清楚这样的一个数据结构在java中是怎么通过代码表现出来的?
既然是结合了数组和一个链表队列,再结合上面的链表的代码表现方式和数组,来尝试分析一下源码:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key; //
V value; //
Node<K,V> next; //节点
.....
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
第一段代码中,明显的看到有个entry
的内部类,这个可以看作是hashmap中的一个单位,包含了key,value ,next,hash(下面会讲)。那这个节点很类似上面的链表的节点,只不是单向的。
第二段代码中,这个就是我们的一个节点数组,可以看到上面有它的注释——在第一次使用中会初始化,并且在需要的时候可以扩容,table
的长度必须是2的平方数。
固定的数值
那在HashMap中有一些是固定懂的值,如下
/**
* The default initial capacity - MUST be a power of two.
* 初始的数组的容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
* 负载因子,也就是当容量到达当前的75%的时候就会进行一个提前的扩容而不会是一个满了再进行扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 当链表超过8的时候,链表会转为红黑树
*/
static final int TREEIFY_THRESHOLD = 8;
那得到如下的图(最右边的是超过阀值的时候,链表转为红黑树,):
hash函数
到这里我们来分析一下,当我加入一个entry
的节点到HashMap
中,HashMap是怎么样规定一个值的存储的位置,首先我们要确定这个节点在数组中的位置。常规是hashcode的值取数组长度-1的膜。那源码中是如下表示的
.....
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
....
可以看到在源码中采用了&
的位运算去计算这样的一个值(n - 1) & hash
如下
//假如hash值现在是343435454 n 为初始化数组大小16
二进制 hash 10100011110000110100010111110
二进制 n-1 01111
//当2者进行一个`&`运算就是一个取模的运算
那这里我们进行一个思考,为什么前面HashMap规定数组的容量一定要是2的一个平方数。
因为在HashMap中我们需要的是用key的一个hashcode值来进行一个计算位置,尽量去依赖hashcode,而当比如容量是15,那会去取01110(14)的一个膜,此时hashcode的最后一位无论是0还是1 ,结果都是 0 ,与它自身的值无关,所以java中规定容量必须是2的平方数,这样取模时低位都是1,取出来的位置完全依赖hashcode
这个时候我注意到一个有意思的事情,这里用来取模的一个hashcode,是经过了一个HashMap的一个hash()方法的处理的如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
来分析一下这个源码,首先我们发现如下
//假如hashcode值现在是343435454
二进制 hashcode 10100011110000110100010111110
hashcode >>> 16 1010001111000
``进行一个亦或运算
因为在HashMap中,不想让一个数组位置下面的链表过长,而其他的位置链表过短,要大家相互差不多。因为取模时对低位的数字依赖高而忽略了高位的数字,如果直接使用hashcode取模,那对最后的结果会集中堆积在几个数组位置上。
put
直接上源码来分析如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //对容量进行初始化 同时记录容量和负载的容量
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //如果当前数组位置没有数,则创建一个node节点放入
else {
//当前位置不为空
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//当数组大小大于一个负载的数量也就是 (容量*0.75)
resize();
afterNodeInsertion(evict);
return null;
}
扩容操作
首先HashMap在到达一个阀值以后,会进行一个扩容,看一下源码
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 //扩大2倍
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//第一次初始化
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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) {
//遍历map
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
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
//对链表<8的下标处理
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;
}
分析:
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//假如hash值现在是343435454 oldCap 为旧数组大小16
二进制 hash 101000111100001101000101 1 1110
二进制 16 1 0000
&运算结果 也就是结果取决与hash的第5位 如果为0,就不变动下标的位置
如果不为0
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
//可以看到是当前下标+一个旧容量 得到一个下标 。 不信的朋友可以手动验证几次
谢谢您的阅读