【Java容器】HashMap从入门到熟悉

1. HashMap从入门到熟悉

1. hash碰撞的解决方案

在这里插入图片描述
HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都有一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。

2. 红黑树优化方案

2.1 为什么是长度为8的时候发生转换

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). And when they become too small (due to removal or resizing) they are converted back to plain bins. In usages with well-distributed user hashCodes, tree bins are rarely used. Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution (http://en.wikipedia.org/wiki/Poisson_distribution) with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are:

0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million

理想情况下,在随机哈希代码下,桶中的节点频率遵循泊松分布,文中给出了桶长度k的频率表。

由频率表可以看出,桶的长度超过8的概率非常非常小。所以作者应该是根据概率统计而选择了8作为阀值,由此可见,这个选择是非常严谨和科学的。

2.2 既然存在链表转换为红黑树,那么是否存在红黑树转换为链表

HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树若桶中元素小于等于6时,树结构还原成链表形式。

  • 红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
  • 还有选择6和8的原因是:
    • 中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

3. 扩容发生的时间,为什么扩容是2倍,扩容的过程

3.1 扩容发生的时间

大于等于阈值—即当前数组的长度乘以加载因子的值的时候,就要自动扩容。

  • 负载因子
    • 过小:容易发生reszie,消耗性能
    • 过大:容易发生hash碰撞,链表变长,红黑树变高

3.2 为什么hashmap底层数组要保证是2的n次方

   	//hash值的计算分为两步:
    //1. 异或运算
    static final int hash(Object key) {   //jdk1.8 & jdk1.7
         int h;
         // h = key.hashCode() 为第一步 取hashCode值
         // h ^ (h >>> 16)  为第二步 高位参与运算
         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //2. 和数组长度与运算,分布到原有数组中
    hash = h&(n-1)

得到 hash 值之后,再与数组的长度-1(length-1)进行一次与运算,因为如果数组的长度是 2 的倍数,那么length-1 的二进制一定是 …00001111…这种形式,也就是前面一定都是 0,后面全是1,那么再与 hash 值进行与运算的时候,结果一定是在原来数组大小的范围内,比如默认数组大小16-1=15 的二进制为: 00000000 00000000 00000000 00001111,某 key 的hash 值为:11010010 00000001 10010000 00100100,那么与上面做与运算的时候,值会对后面的四位进行运算,肯定会落在0~15 的范围内,假如不是 2 的倍数,那么 length-1 的二进制后面就不可能全是 1,做与运算的时候就会造成空间浪费。

3.3 扩容的具体过程

  • 开辟了新的数组空间
  • 元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

在这里插入图片描述

4 既然存在扩容,是否存在缩容

没有缩容机制,没有看到与resize()对应方法。

5 HashMap和HashTable、HashSet、LinkedHashMap

在这里插入图片描述

  • Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以ConcurrentHashMap替换。
  • LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
  • TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

5.2 HashMap和HashSet的区别

在这里插入图片描述

6. Hashmap为什么是线程不安全的【死锁分析】

  • 表面原因
    • Hashmap的方法没有使用synchronized进行同步
  • 实际原因
    • 如果能找到并发环境下的问题,就能证明是不安全的
    • 并发环境下,hashmap进入扩容的时候容易造成Entry链成环,在查询等操作的时候容易造成死循环

7. TreeMap和HashMap有什么区别

使用Iterator迭代器遍历的时候,HashMap的结果是没有排序的,而TreeMap输出的结果是排好序的。

参考资料

Ref1:https://tech.meituan.com/2016/06/24/java-hashmap.html

https://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章