HashMap的存储结构可以理解成数组+链表的结构,但是也不完全这样理解,因为数组查找元素的时候,需要进行遍历,严格来说,数组是一种顺序查找,当数组进行查找的时候,第一个元素找到的时间肯定比最后一个元素找到的时间短。而HashMap不同,因为存在一个Key,这个Key很关键,有了它就可以通过Hash函数找到散列的地址,在找到散列地址下的值,所以查找方式和数组不同。再接着理解链表的结构,Hash函数在计算散列值(也就是散列地址)的时候,会出现散列值相同的情况(碰撞),这个时候就会出现存储在同一个散列地址下的元素,这个解决办法就是通过链表的形式来进行存储多个同散列地址下的元素。
<分割线----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------分割线>
但是HashMap的精华在于,散列值和索引值的求取。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
这是JDK1.7的Hash函数,利用Hash函数的若干次位运算,将HashCode变的松散,这样得到的Hash值可以达到充分的散列均匀。
static int hash(object key) {
int h;
return (key == null)?0:(h = key.hashCode())^(h>>>16));
}
这是JDK1.8的Hash函数,对1.7进行了优化,但是本质没有变化。
对索引值的求取,HashMap默认table(上面所说的类似数组的结构)的长度是16,在通过扰乱后的Hash值进行索引值的计算,也就是table的散列地址
static int indexFor(int h, int length) {
return h & (length-1);
}
这里需要说明的是length就是我们的table的长度,h就是我们扰乱后的Hash值,对正常来说,我们将Hash值与长度进行取模,就OK了,但是取余非常慢,我们还有更高效的相与&,进行位相与比取模要快上许多。到这里,还需要了解的是length最好是超过HashMap容量最小2的n次幂,至于为什么要进行减1,那是因为length是偶数的话,进行相与的时候,偶数二进制最低位为0这样会导致table【0】上会散列很多值,而且会使空间很浪费,减1后,奇数二进制最低为1,相与的时候,就会均匀分布。
另外,如果内存足够大,可以考虑使用大的length,这样可以使散列的更加松散,碰撞的几率更加低,get的速度就会更加快,但是会使迭代器的速度变慢,所以在使用的时候预估一下容量是非常有必要的。
<分割线----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------分割线>
还有一点,HashMap是线程不安全的,线程安全可以使用HashTable,那么HashMap线程不安全怎么体现?
如果我有两个线程同时put那么刚好碰撞到一起,那么对出现的覆盖的情况,有个put的数据会被覆盖丢失,当两个线程同时对HashMap进行扩容,在写入新的数据在扩容后的HashMap里面的时候,就会其他线程就会丢失数据。
以上就是我学习HashMap的心得,以后有新的会继续补充。。。
------------------------------------------------------------------------------------------------------------------------------------
2017/9/25 补充说明:HashMap在多线程下不安全,因为其中put方法会很容易发生死循环,具体信息学习笔记重HashMap死循环连接。