HashMap设计的初心是找到一种方法,可以存储一组键值对的集合,并能够快速的实现查找元素。
Map的定义:是将键映射到值的对象
HashMap中是利用内部类Node来定义存储这个键值对的,之后利用Node数组来存储HashMap数据结构,那我们都知道数组之所以能够快速的查找,是因为其具有索引(数组下标)直接定位到对应的存储桶(数组所存储对象的位置)。而在HashMap中为了能够利用索引来查找相应的Key,我们需要建立一种Key->index的映射关系。这样每次我们要查找一个key时,首先根据映射关系,计算出对应的数组下标,然后根据对应的数组下标,直接找到对应的key-value对象,这样基本能以O(1)的时间复杂度得到结果。
而在这里,将key映射成index的方法称为hash算法,我们希望它能将key均匀分布在数组中。
而使用Hash算法同时也补足了数组插入和删除性能差的短板,我们都知道,数组之所以性能差是因为他是顺序存储的,在一个位置插入节点或者删除节点需要一个个移动他的后续节点腾出位置或者覆盖位置。
而使用hash算法后,数组不在按照顺序存储,插入删除操作只需要关注一个存储桶即可,而不再需要额外的操作。
而在我们实际操作中,我们所使用的的Hash算法虽然能够将key均匀分布在数组中,但是它只能够尽量的做到,并不是绝对的,更何况我们的数组大小有限,中间可定会应为Hash算法就两个不同的Key映射成同一个index值,这就产生了Hash冲突,也就是两个Node要存储在数组中的同一个位置中该怎么办
在在解决这个Hash冲突的过程中,我们经常采用的方法是通过选择链地址法来解决,即在产生冲突的存储桶中改为单链表存储。
所以采用了HashMap中的Node类的基本架构
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;
}
...
}
我们都知道, 链表查找时,只能通过顺序查找来实现,因此时间复杂度为O(n),
这就会导致如果key值被Hash算法映射到一个存储桶上,将会导致存储桶上的链表长度越来越长,此时,数组的查找会逐渐退化为链表查找,所以时间复杂度就变为了O(n)。
为了解决这个问题,在Java1.8之后,单链表长度超过8之后,将会自动的将链表转化为红黑树,达到将复杂度降为O(logN)的时间复杂度。从而提高查找性能。
在前面的时候,我们都知道,解决Hash冲突有两个方法,利用链表的形式来解决,但是,还有一个客观的因素就是数组本身的长度,前者不管虽然一定程度上的可以解决Hash冲突,但他所带来的的效益并不是很高,当到达一定的程度之后,不得不采取扩大HashMap本身数组的大小就成为了首选。那到底在实际过程中我们应该如何去扩容,以及每次扩容多少较为合适那。
所以,到现在,我们就必须对扩容做一个了解。
数组扩容是一个很耗费CPU资源的动作,需要将原数组的内容复制到新的数组中,因此,如果扩容动作过于频繁,其必然会导致性能降低。
如何做到合适的扩容,JDK源码resize函数中有实现
未完待续。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
我们知道HashMap中的Key是泛型,所以,我们在实际应用过程中,我们需要将Object先转化为int,之后,再转化为index。所以,我们会首先通过Hash算法将key映射成整数,然后将整数映射成为有限的数组下标。通常在将整数映射成为有限的数组下标过程中,我们都是采用对数组长度的取模运算。即是
Key.hashCode()%table.length
那么HashMap是这样做的的吗?以下是HashMap的散列算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
Int类型是32位,h^h>>>16,其实就是讲Hashcode的高16位和低16位进行异或运算。这样做是为了充分利用高半位和低半位的信息,对低位进行了扰动,目的就是为了该HashCode 映射成数组的下标时更加均匀。详细的解释可以参考https://www.zhihu.com/question/20733617/answer/111577937
从这个HashMap中key值可以为NULL,且NULL值一定存储为数组的第一个位置(Why)
我们上面在对int转化成index的过程中,我们通常会想到的是MOD方法来实现,但是,由于HashMap的数组长度2^n,此时,利用下面公式可以做到性能的提升。
任意整数对2^n取模等效于:
h % 2^n = h & (2^n -1)
这样我们就将取模操作转换成了位操作, 而位操作的速度远远快于取模操作.
为此, HashMap中, table的大小都是2的n次方, 即使你在构造函数中指定了table的大小, HashMap也会将该值扩大为距离它最近的2的整数次幂的值.
那我们就来看一下HashMap函数是如何每次构造出距离它最近的2的整数次幂的值。
观看源代码其中最为重要的部分是在
this.threshold = tableSizeFor(initialCapacity);
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
tableSizeFor方法用于找到大于等于initialCapacity的最小的2的幂
我们接下来就来看一下这个代码的是如何来找到的,
当一个32位的整数不为0的时候,32bit中至少有一个位置为1,上面的2个移位操作目的在于从最高位的1开始,一直到最低位的所有bit全部设为1,最后再加1(注意,一开始的时候,是先cap-1的)则得到的数就是大于等于initialCapa的最小的2次幂。详细的请参考这篇博客https://blog.csdn.net/fan2012huan/article/details/51097331