孤尽训练营打卡日记day18--HashMap初识

前言

        Map是我们平时开发时候经常用到的一种数据结构,我们都知道他是一种 <key,value> 的模型。Map中最常用的是HashMap,因为他是线程安全的,为什么他是线程安全的呢,他和Map的区别在哪呢,为什么线程安全推荐使用CurrentHashMap,今天我们一起来学习一下HashMap的源码。

HashMap是什么?

  • HashMap是一种常用的java容器,它继承了AbstractMap,实现了Map,Cloneable,Serializable;
  • HashMap在java7里是一种数组+链表的数据结构,在java8优化成数组+链表/红黑树,通过先通过hash算法算出hash值,再与上数组长度-1,计算出数组下标  
  • 它的节点也是一种key-value 的数据结构,key唯一,key和value都允许为null,因为key要唯一,所以一个HashMap里只会有一个key为null的节点,并且在java7里,这个节点放在链表的头节点

Hash:

        基本原理就是把任意长度的输入通过Hash算法变成固定长度的输出。这个映射的规则就是Hash算法,而原始数据映射后的二进制串就是hash值。

Hash的特点:

  • 不可逆,无法根据hash值反向推导出原始数据
  • 相同的数据会得到相同的hash值(值相等,hash值一定相等,hash值相等,值不一定相等;hash碰撞)
  • hash算法高效
  • hash算法的冲突概率小

思考:为什么会产生hash碰撞?

答:经过hash算法计算后的结果范围小于输入的值,肯定会存在不同的值被映射成相同hash值得情况(抽屉原理)

思考:为什么使用会有链表,红黑树

答:数组在内存中占一片连续的内存空间,通过索引下标查找快,但是数组的增删慢,当数值很多时,增删节点效率低,所以使用hash表,利用hash值与数组长度的与计算出数组下标,在增删节点的时候,就不在需要对整个数组进行改变;引入了链表是因为当数组下标重复,也就是有hash碰撞时,数组上的节点将以链表的形式存放;链表的节点是游离在内存空间中的,每一个节点保存下一个节点的引用,而链表在查询时效率低,比如查找的元素在链表的最后,则时间复杂度为O(n),所以当链表长度大于 8 且元素个数大于 64 的时候,将会树化。

思考:为什么链表长度大于8 的时候树化?

答:树化的过程很慢,消耗资源,应尽量避免树化,链表上的值都是hash碰撞产生的,一组数值能产生 8 次hash碰撞的概率很低

官方说明:

实现了Map类,并允许空value和空key.

        这个实现为基本操作(get和put)提供恒定的时间性能,假设散列函数在桶中适当地分散元素。集合视图上的迭代所需的时间与HashMap实例的“容量”(桶的数量)加上其大小(键值映射的数量)成比例。因此,如果迭代性能很重要,那么不要将初始容量设置得太高(或者负载系数太低),这一点非常重要。

        HashMap的实例有两个影响其性能的参数:初始容量和负载因子。初始容量是哈希表中的桶数,初始容量就是哈希表创建时的容量。负载因子是一个度量哈希表在其容量自动增加之前可以达到的完整程度。当哈希表中的条目数超过负载因子与当前容量的乘积时,哈希表将被重新哈希化(即重建内部数据结构),因此哈希表的存储桶数是原来的两倍。

        初始化容量就是数组长度?达到扩容阈值之后,数组扩容,那元素个数肯定是在数组长度内,也就是说,会存在数组里的节点为null的情况,在产生hash碰撞的时候。

        一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的折衷。较高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置初始容量时,应考虑Map中的预期条目数及其负载因子,以尽量减少再rehash操作的次数。如果初始容量大于最大入口数除以负载因子,则不会发生rehash操作。

        如果许多映射要存储在一个HashMap实例中,那么创建一个足够大的容量将使映射得到更有效的存储,而不是让它根据需要执行自动rehash以增加表。

        注意,这个实现是不同步的。如果多个线程同时访问一个哈希映射,并且至少有一个线程在结构上修改了该映射,则它必须在外部同步。(结构修改是指添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键相关联的值不是结构修改。)这通常是通过对自然封装映射的对象进行同步来完成的。如果不存在这样的对象,则应该使用{@link Collections\synchronizedMap对映射进行“包装”集合.synchronizedMap}方法。最好在创建时执行此操作,以防止意外的不同步访问映射:

                                    map m=集合.synchronizedMap(new HashMap(…);

        这个类的所有“集合视图方法”返回的迭代器是<i>fail-fast</i>:如果在迭代器创建之后的任何时候对映射进行了结构修改,除了通过迭代器自己的<tt>remove</tt>方法,迭代器将抛出{@link ConcurrentModificationException}。因此,在并发修改的情况下,迭代器会迅速而彻底地失败,而不是在将来某个不确定的时间冒着任意的、不确定的行为的风险

        注意,迭代器的fail-fast行为不能得到保证,因为一般来说,在存在不同步的并发修改时,不可能做出任何硬保证。Fail fast迭代器尽最大努力抛出ConcurrentModificationException</tt>。因此,编写一个依赖于这个异常来保证其正确性的程序是错误的:<i>迭代器的快速失效行为应该只用于检测错误。

实现说明:

        这个映射通常充当一个哈希表,但是当容量变得太大时,它们会被转换成TreeNodes,每个容器的结构与java.util.TreeMap相似. 大多数方法尝试使用普通的bin,但在适用的情况下(只需通过节点的签入实例)中继到TreeNode方法。TreeNodes的存储箱可以像任何其他的一样被遍历和使用,但是在人口过多的情况下还支持更快的查找。然而,由于正常使用中的绝大多数容量并不是过多的,所以在表格方法的过程中,检查是否存在树型容器可能会延迟。

        树型容器(即元素都是树型节点的容器)主要通过哈希代码进行排序,但是在链表的情况下,如果两个元素属于同一个“类C implements Comparable<C>”,则使用它们的compareTo方法进行排序。(我们通过反射保守地检查泛型类型以验证这一点——请参见方法comparableClassFor)。当键具有不同的哈希值或可排序时,树容器的额外复杂性对于提供最坏情况下的O(logn)操作是值得的,因此,在hashCode()方法返回分布不好的值的意外或恶意使用以及许多键共享一个hashCode的情况下,性能会正常下降,只要它们也具有可比性。(如果这两者都不适用,与不采取预防措施相比,我们在时间和空间上可能会浪费大约两倍的时间和空间。但是,已知的唯一案例都源于糟糕的用户编程实践,这些实践已经非常缓慢,几乎没有什么区别。)

        因为treeNode大约是常规节点的两倍,所以我们只在bin包含足够多的节点以保证使用时才使用它们(参见treeFy_THRESHOLD)。当它们变得太小(由于移除或调整尺寸),它们会被转换回普通的垃圾箱。在使用分布良好的用户哈希代码时,很少使用树容器。理想情况下,在随机hashcode下,bin中节点的频率服从Poisson分布(http://en.wikipedia.org/wiki/Poisson_发行版)默认调整阈值为0.75时,参数平均约为0.5,但由于调整粒度的原因,差异较大。忽略方差,列表大小k的预期出现次数为(exp(-0.5)*pow(0.5,k)/阶乘(k))。第一个值是

*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

*多:不到千万分之一

        树状容器的根通常是它的第一个节点。然而,有时(目前仅迭代器.remove)链接可能在其他地方,但可以在其他地方恢复父方法树根()).

(8次hash碰撞的概率很小)

        所有适用的内部方法都接受哈希代码作为参数(通常由公共方法提供),允许它们相互调用,而无需重新计算用户哈希代码。大多数内部方法也接受一个“tab”参数,通常是当前表,但在调整大小或转换时可能是新的或旧的。

        当bin列表被树化、拆分或未查询时,我们将它们保持在相同的相对访问/遍历顺序(即字段节点.下一个)以更好地保留局部性,并稍微简化对调用的拆分和遍历的处理迭代器.remove. 当在插入时使用比较器时,为了保持整个重新平衡的顺序(或者与这里要求的一样接近),我们比较类并将hashcode标识为断开连接符。

        普通vs tree模式之间的使用和转换由于子类LinkedHashMap的存在而变得复杂。下面的钩子方法定义为在插入、删除和访问时调用,这些钩子方法允许LinkedHashMap内部保持独立于这些机制。(这还要求将映射实例传递给可能创建新节点的某些实用程序方法。)

纸上得来终觉浅,绝知此事要躬行。   --《冬夜读书示子聿》陆游

参考文档:JDK1.8源码注解

猜你喜欢

转载自blog.csdn.net/qq_35056844/article/details/121296643