hashmap的数据结构为数组、链表+红黑树(在链表节点数量超过8时链表会变成红黑树),如下图
那每个格子里面到底是存储的什么东西呢?hashmap肯定是存储的key/value结构的数据
里面存储了key、value,既然是链表有节点,那是不是就应该有节点指示下一个节点的指示?还有当前的位置?不然当取得时候如何存取?
总共4个属性:hash,key,value,next,hash表示当前格子的位置,next表示指向下一个节点,是一个单向链表。那么在java中以面向对象的概念来说,哪些比较适合呢?entry、node
看看hashmap的源码,node为内部类:
这个就是每个格子,也就是node
那既然有数组,数组是怎么表示呢?正常的推理就是node数组 Node[],就是下图,在hashmap默认初始化的时候,默认的长度是16,长度是2^n。必须是2^n,后面会讲
那当存储的时候,怎么存储?可能有人说使用new Random().nextInt(0,15)
当使用Random这种方式的时候,第一次存储存储在1的位置上,第二次如果还是1呢?那第三次如果还有可能是1呢?就会出现以下的情况,全部放在1的下标桶里面。一个桶出现很多很多的节点。如下图
如果按照上面的设计,那是不是其他的全部都是空着的,节点分布也不均匀,jdk设计作者肯定不会这样来设计。
那就要另寻他法,用其他的算法来设计===》hash算法
通过hash算法来确定一个整形数,put的时候,会询问数组,如果当前桶是空着的,直接放入桶里面,如果发生hash碰撞,则放入链表节点,这样还是不能避免不重复,但能分布得更均匀。
当一个桶的链表节点太长的时候,会将深度改为红黑树,红黑树也算一种二叉树。
注意在jdk7及以下是没有红黑树的,在jdk8版本jdk的设计作者才加入了这个算法。看下图
红黑树的要求,以及满足红黑色的条件:
null
(树尾端)的任何路径,都含有相同个数的黑色节点。
什么时候转换成红黑树?
当超过8个节点的时候,会生成红黑树的树状结构
当节点数量6及以下的时候,会还原成链表
Map hashMap = new HashMap();
hashMap.put("ypp", 666);
/**
* Constructs an empty HashMap with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
当new hashmap的时候,并没有初始化,只是初始化了一个负载因子,这是为了扩容而准备的
进入hashmap的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 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);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)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)
resize();
afterNodeInsertion(evict);
return null;
}
进入putVal()方法,首先判断table是否为空等,当put的时候,当前数组其实是一个空的。这个table就是Node
final Node[] resize() {
Node[] 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) // 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[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node 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)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node 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;
}
会进入到以下两行代码,以下两行代码,不用说,都能看懂,初始化数组的长度16和扩容初始化12,其他代码先不看
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
这里使用的位运算,1向左位移4==》10000 转换成10进制就是16,然后16*负载因子0.75=12.
在我们平时写代码的时候基本都是十进制,但计算机十进制最后还是会转换为二进制,所以它直接使用了二进制提高效率,虽然一个地方不起眼,但是作为一个框架或者一门语言,地方多了,效率可想而知是可以提高不少的。但是在开发中建议不要用二进制去增加开发的复杂度,毕竟开发用的地方并不多!
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
当初始化完成后回到刚才的put()方法,有一个hash(key)的方法传入putVal()这个方法里面,hash(key)的位置得出一个hash值,这里还是使用的位移+异或运算,喜欢看源码的同学会发现很多的框架等都会使用位移以提高效率。
回到putVal()方法,比较重要的源码点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
得出来的hash这里用到了与运算,为什么还要-1?并且不是使用的hash%16的这种方式
n-1=15 n就是数组的长度 15的二进制为01111
之前计算的hash值右移16位不管怎么样最多都是 1010110101110110111011101011011这样的格式,和01111进行与运算
1010110101110110111011101011011
01111
不管如何换成十进制最小0最大还是15,那为什么它要使用与运算而不是hash%16取模呢?一个字:快,两个字:效率
if ((p = tab[i = (n - 1) & hash]) == null)
这里判断就是这个桶下标是否为空,如果为空,当前node直接放进去:tab[i]=newNode(hash, key, value, null); null是节点指向下一个节点。这里为空是因为当前就这一个节点,并没有其他节点,当后面如果有其他节点进来,会改变它的值指向下一个节点
如果不为空,进入以下else代码:
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)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)
resize();
afterNodeInsertion(evict);
return null;
代码1 if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
代码2 if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
代码1当hash值一样的时候,key相同。代码2直接替换并返回
代码3
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
代码3判断当前这个节点下面是否为红黑树,进行红黑树相应处理,下面截图可以看到父节点、左节点、右节点以及红色节点等
代码4
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;
}
代码4就是链表的情况,循环当前下标所有节点,判断哪个节点的next为空,就把自己放入到那个节点后面去
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
如果节点数量超过了8,就把当前链表转换为红黑树,并跳出循环
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
肯定会存在着不够用的情况,那如何扩容?
resize()
putVal()
在初始化的时候,hashmap内部记录了当前扩容的相关参数
在put添加数据的时候,putVal()方法结尾判断了当前数组实际使用大小,当初始化的hashmap长度16,使用超过了12会再次调用resize()方法进行扩容扩容是以2倍扩容,来保证长度必须是2^n。16变为32 threshold变为24.当put的时候还是上面的流程。
所以resize()方法有两个主要功能,初始化和扩容
扩容之后会重新计算,分布节点
以上只是hashmap其中的一部分,主要的,写了半个多小时。时间有限,已经凌晨1点半,就不写了。如果帮助到你,点个赞
最后附上阿里相关面试题:
addAll
、removeAll
)。Collections.sort(List list)
;sort(List list, Comparator super T> c)
;TreeSet
底层是 TreeMap
,TreeMap
是基于红黑树来实现的。