哈希表保存数据:
HashMap保存数据时是使用<K,V>(键值对)保存的,键值对是一种映射关系,一个Key对应一个Value值。K值具有唯一性,当我们保存相同的K值时,Value值会被新的Value所覆盖,不会存在有两个相同的K值。
JDK中哈希表的底层原理:
HashMap查找的效率极高,时间复杂度O(1),这是一个非常高的查找效率,哈希表也是典型的用空间来换时间的一个数据结构。首先我们要明白哈希表底层是怎么实现的。在JDK8中,哈希表的底层是数组+链表/红黑树。当达到一定条件的时候链表会转换成红黑树,这个我们等等再进行介绍。而在JDK8之前的哈希表都是使用数组+链表的结构,没有引入红黑树这个数据结构。既然哈希表的底层是数组的一种形式,那我们知道数组在知道索引的情况下,查找的效率就是O(1),所以我们隐隐约约可以感觉到,哈希表查找的效率这么高,和索引离不开关系。哈希表通过哈希函数对Key进行哈希得到一个哈希值,这个哈希值对应的就是数组的索引,然后把这数据放入数组索引对应的位置,然后我们查找的时候也就是使用这个哈希值进行搜索,所以查找的时间复杂度可以达到O(1)。
JDK哈希表底层的一些属性:
DEFAULT_LOAD-FACTOR:通过注释我们可以知道,这个数据代表了一开始的数组长度为16,而且扩容必须是2倍扩容。
MAXIMUN_CAPACITY:最大容量为2^30这么大。
DEFAULT_LOAD_FACTOR: 负载因子大小(存储元素/数组长度)
TREEIFY_THRESHOLD:这个代表了当我们的链表长度为8的时候就会树化,但是树化还有一个条件。
UNTREEIFY_THRESHOLD:当链表长度为6的时候,就会取消树化操作。
MIN_TREEIFY-CAPACITY:这也是我们树化的第二个条件,那就是数组长度>=64和链表长度>=8时才会进行树化操作。
哈希函数:
通过查看JDK的哈希表的源码我们可以看到JDK的哈希值是先使用JDK中的hashCode函数把Key转换成一个哈希值,然后将这个哈希值右移16位与一开始的哈希进行异或,这样会使得的出来hash值比较均匀分布,最后我们通过(n-1)&hash值得到最后索引的位置,其实(n-1)&hash这个操作本质上就是取模操作,只是位运算的速度更快,所以没有直接进行取模。这样我们通过整个hash函数就可以得到要存放元素位置的索引,从而进行高效查找。
关于哈希表存储数据:
那我们的链表又是来做啥的呢,接下来我们就来介绍链表的作用,当我们不同Key值经过哈希函数的计算可能会得到一样的hash值,我们把这样的事情叫做哈希冲突。哈希冲突在理论上一定是会发生的。我们一般有两种办法来解决哈希冲突:开散列和闭散列两种方式。我们JDK是使用开散列来解决这个问题,开散列也就是我们俗称的哈希桶,在JDK8中我们通过尾插法将hash值相同的元素连接在链表的尾部,这样如果我们进行搜索我们不需要遍历整个数组,而只需要遍历链表来寻找,大大增加了查找的效率,当链表过长的时候就会将链表树化成一棵红黑树来增加查找效率。当我们存储完之后,发现超过了负载因子,那这时候数组就会进行扩容,而扩容之后原本的数组对应的索引存放的元素会进行重新hash,放在新的索引位置。这里我只截取一部分源码。
hashCode()和equals()方法:
为什么我们重写hashCode()方法的时候也需要重写equals()方法呢。我们需要明确,当我们的hashCode相同的时候,不代表equals会相同,我们举一个例子,比如我们有两个数据:美丽和美景。我们假设hashCode都是美这个字,这时候我们发现equals之后会显示false,所以hashCode相同equals不一定相同,而equals相同,hashCode一定相同。那有什么作用呢,当我们存储数据时,发现hash值相同,但是我们需要保证Key的唯一性,所以我们需要借助equals方法来帮助我们比较这个Key是否在该链表中存在,如果存在则覆盖旧的Value值,不存在,则进行尾插法。这时候equals()方法的价值就体现出来了。