深入浅出HashMap

作者: 悟成 | 来源:发表于2018-09-18 16:55 被阅读2次

        HashMap作为Java的一种特殊类型,在我们的编码过程中被广泛使用,专门用于存放类似于(key:value)这样的数据。它就像一个糖罐子,只是它里面每项数据都由两个值组成的。在HashMap出来之前,java已经存在可以用于存放(key:value)数据类型的对象,如Dictionary和Hashtable,由于他们先前设计上的缺陷,目前Dictionary已经废弃了,而Hashtable也不怎么使用了。目前技术条件下的编码过程中,如果遇到(key:value)数据类型程序员们优先考虑使用HashMap类型。接下来我们从顶层设计开始由浅入深一步一步认识HashMap。下图是Hash的设计类图:

HashMap类图

        从图中可以看出HashMap是继承AbstractMap,并且HashMap的存储内容是Entity实体组成的,且都实现了自己特有的方法。那么具体使用的时候HashMap数据在内存中是如何存储的呢?啥都不说我们先看下图:

hashMap存储

        紫色部分即代表哈希表本身(其实是一个数组),数组的每个元素都是一个单链表的头节点,链表是用来解决hash地址冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中保存。HashMap设计初衷为了提升通过key获取value的效率,所以HashMap的Entity设计初衷是通过二叉树-红黑树进行存放的,只是HashMap的内容过多就退化为单项链表了。接下来我们去看一看具体操作的流程,结合程序设计的流程图HashMap的Put操作是如何实现的呢?

HashMap put操作

        图中显示了HashMap的一个Put操作的流程,应用程序的put语句的后台实现原理。这里面涉及到两个HashMap重要的操作resize-扩容和hash值计算。接下来分别讲解这两个操作:

        扩容,简单说就是HashMap有一张Hash表,Hash表是由数组构成,在Java中数组的初始化需要制定长度,在HashMap中默认初始化大小为16,即只能存放16个(key:vaule)内容。16是开发HashMap的作者根据大多应用场景得出的能应付多数应用场景的值,在数据量较大且无法估计HashMap大小的场景中就需要启用扩容机制。HashMap每次扩充,容量变为原来的2倍。

        扩容涉及到扩容的两个重要参数initialCapacityloadFactorinitialCapacity设置初始化HashMap大小的值,默认是16。loadFactor是加载因子,默认是0.75。当存储个数threshold即size > initialCapacity *loadFactor,hashmap内部resize方法就被调用。同样initialCapacityloadFactor也可以在实例化HashMap的时候调用构造函数进行初始化,但需要注意一下两点。

1.initialCapacity容量较大,那么会使迭代器效率降低。所以理想的情况还是在使用HashMap前估计一下数据量。

2.加载因子默认值是0.75,是JDK权衡时间和空间效率之后得到的一个相对优良的数值。如果这个值过大,虽然空间利用率是高了,但是对于HashMap中的一些方法的效率就下降了。

        根据HashMap的实现,它在每次扩容后的容量是原先的一倍。扩充结果都为2的幂次方大小。即HashMap总是使用2的幂作为哈希表的大小。这样做可以成倍提升计算Hash值的效率,为什么这么说呢?

        因为HashMap大小是2的幂次方,所以它使用取模计算时,可以直接使用位运算来得到结果,效率大大高于以前使用除法的效率。比如在HashMap执行增加、删除、查找键值对时,定位到哈希位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到key的hashCode值;2)将hashCode的高位参与运算,重新计算hash值;3)将计算出来的hash值与(table.length - 1)进行&运算。

        虽然使用HashMap在效率上会有提升,但是为了提升效率,HashMap在实现过程中未进行线程安全性限制,所以HashMap是线程不安全的。具体的线程不安全体现在以下两方面:

第一,如果多个线程同时使用put方法添加元素

假设正好存在两个put的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。

第二,如果多个线程同时检测到元素个数超过数组大小*loadFactor

这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。

        HashMap给出了他自己的解决并发办法,多线程需要使用HashMap建议采用concurrent并发包下的concurrentHashMap。

        今天和大家简单的分享了Java的HashMap,当然HashMap还有很多特性我一文不可能全部列举完整,希望和大家一起交流学习,共同进步。

相关文章

  • HashMap在JDK7和JDK8中的区别

    在[深入浅出集合Map]中,已讲述了HashMap在jdk7中实现,在此就不再细说了 JDK7中的HashMap ...

  • 最通俗易懂的 HashMap 源码分析解读

    HashMap 作为最常用的集合类之一,有必要深入浅出的了解一下。这篇文章会深入到 HashMap 源码,刨析它的...

  • 深入浅出HashMap

    HashMap作为Java的一种特殊类型,在我们的编码过程中被广泛使用,专门用于存放类似于(key:val...

  • 深入浅出HashMap

    Map map是一种 key/value组织的数据结构。在Java中Map是一个接口类,其实现类比较常用的有:Ha...

  • 深入浅出学Java-HashMap

    一、概要 HashMap在JDK1.8之前的实现方式 数组+链表,但是在JDK1.8后对HashMap进行了底层优...

  • HashMap了解一下

    前言 HashMap HashMap类继承图 HashMap属性 HashMap构造函数HashMap(int i...

  • HashMap源码

    eg: HashMap hashMap = new HashMap<>(); hashMap.put(1,"QG...

  • 2018-03-12

    HashMap in Java HashMap in Redis HashMap in Golang

  • 深入浅出HashMap扩容死循环问题

    一.问题 众所周知,HashMap是线程不安全的,在并发使用HashMap时很容易出现一些问题,其中最典型的就是并...

  • 深入浅出HashMap的设计与优化

    HashMap 的实现结构 了解完数据结构后,我们再来看下 HashMap 的实现结构。作为最常用的 Map 类,...

网友评论

    本文标题:深入浅出HashMap

    本文链接:https://www.haomeiwen.com/subject/gawpnftx.html