HashMap详解

【基本介绍】

链接相关知识:Map, List, Set基本介绍。

   

HashMap是一个散装桶(数组或链表),它存储的内容是键值对(key-value)映射。

HashMap采用了数组和链表的数据结构,继承了数组的线性查找和链表的寻址修改。

   

HashMap是非synchronized的,所以很快。

   

HashMap可以接受null键和值,而HashTable不能。

   

ConcurrentHashMapHashTable因为在多线程并发的情况下,put操作时无法分辨key是没找到为null,还是key值对应的valuenull,所以基于这类情况不允许键值为null

HashMap是线程不安全的,也就无所谓了。

 

    

【工作原理】

HashMap是基于hashing的原理,咱们使用putkeyvalue)把对象存储到HashMap中,使用getkey)从HashMap中获取对象。

   

当我们使用put方法传递键值对的时候,先对键调用hashCode方法,计算并返回的hashCode……

HashMapbucket中存储键对象和值对象,作为Map.node

   

以下是HashMap的初始化(简单模拟):

Node table = new Node[16] //散列桶初始化,table

class Node {

hash; //hash

key; //

value; //

Node next; //用于指向链表的下一层(产生冲突,用拉链法)

}

   

以下是具体的put过程(jdk1.8

1. KeyHash值,然后再计算下标

   

2. 如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)

   

3. 如果碰撞了,则调用equals() 比较value,相同则替换旧值,不同则以链表的方式链接到后面

   

4. 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表

   

5. 如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

   

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

   

【如何减少碰撞】

扰动函数可以减少碰撞,原理是如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这就意味着存链表结构减小,这样取值的话就不会频繁调用equal方法,这样就能提高HashMap的性能。(扰动即Hash方法内部的算法实现,目的是让不同对象返回不同hashcode

   

使用不可变的声明作final的对象,并且采用合适的equals()hashCode()方法的话,将会减少碰撞的发生。

   

为什么String, Interger这样的wrapper类适合作为键?因为Stringfinal的,而且已经重写了equals()hashCode()方法了。

   

不可变性是必须的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。

   

hash函数的实现】

hashmap中要找到某个元素,需要根据keyhash值来求得对应数组中的位置。如何计算这个位置就是hash算法。

   

hashmap的数据结构是数组和链表的结合,所以我们希望这个hashmap里面的元素位置分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。

   

所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。

   

但是,"模"运算的消耗还是比较大的,咱们可以看一下jdk1.8怎么做的:

static final int hash(Object key) {

if (key == null){

return 0;

}

int h;

h=key.hashCode(); //返回散列值也就是hashcode

// ^ :按位异或

// >>>:无符号右移,忽略符号位,空位都以0补齐

//其中n是数组的长度,即Map的数组部分初始化长度

return (n-1)&(h ^ (h >>> 16));

}

   

简单来说

1. 16bt不变,低16bit和高16bit做了一个异或(得到的hashcode转化为32位的二进制,前16位高16bit和后16位低16bit做了一个异或)

2. (n-1)&hash=->得到下标

   

【链表过深的问题讨论】

拉链法会导致链表过深,所以选择红黑树作为替代。

没有选择二叉查找树的原因是因为在部分情况下,二叉查找树会变成一条线性结构,不能从根本解决问题。

   

红黑树属于平衡二叉树,但是为了保持平衡需要付出一定的代价,红黑树在插入新数据后,可能需要左旋,右旋,变色等操作保持平衡,但是总体来讲该代价较遍历线性数据要小。

所以当长度超过8的时候,引入红黑树。

   

【关于红黑树】

·每个节点非红即黑

·根节点总是黑色

·如果节点为红色,则其子节点是黑色

·每个叶子节点都是黑色(NIL节点)

·黑色高度一致(从根节点开始的每条路径包含的黑色节点相同)

   

【解决Hash碰撞的其他方法】

开放地址法

当冲突发生时,使用某种探查方法在散列表中形成一个探查序列。沿此序列一直向下查找,直到查到有效的地址。

   

【超过负载因子】

默认的负载因子是0.75.

也就是说,当一个map填满了75% bucket的时候,和别的集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,用以重新调整map的大小,并将原来的对象放入新的bucket数组中。

   

这个过程叫做rehashing,最后的值只会有两种可能,原先的位置或者原先的位置+size

   

rehashing

重新调整HashMap大小的时候,有可能存在条件竞争,因为如果两个线程同时同时发现HashMap需要rehashing的时候,会同时尝试resize

如果条件竞争发生了,那么就死循环了。

   

rehashing的过程中,存储在链表中的元素的次序会反过来,这是由于为了避免尾部遍历HashMap不会将元素每次都放在尾部,而是放在头部。

   

HashTable

数组 + 链表方式存储

默认容量:11(一般为质数)

put

索引计算 : key.hashCode() & 0x7FFFFFFF% table.length

如果在链表中找到了,则替换旧值,未找到则继续

当总元素个数超过容量*加载因子的时候,扩容为原来的2倍,重新散列

新元素插入到链表头部

修改HashTable内部共享数据的方法添加了 synchronized保证了线程安全。

   

【区别】

默认容量不同。

线程安全性,HashTable是线程安全的。

HashTable比较慢。

   

CocurrentHashMap

ConcurrentHashMap同步性能比HashTable更好,它仅仅根据同步级别,map的一部分进行上锁。

ConcurrentHashMap可以用来替代HashTable,但是后者具有更好的线程安全性。

HashTable的大小增加到一定程度的时候,性能会急剧下降,因为迭代时会锁定很长时间。

   

但是ConcurrentHashMap引入了分割segmentation),无论它变得多大,都只需要锁定map的其中一部分,这时候其他线程可以访问其未锁定部分。

posted @ 2020-05-26 20:44  流年的夏天  阅读(500)  评论(0编辑  收藏  举报