2019独角兽企业重金招聘Python工程师标准>>>

Java为数据结构中的映射定义了一个接口java.util.Map

此接口主要有四个常用的实现类,分别是HashMap、 Hashtable、LinkedHashMap和TreeMap。

下面针对各个实现类的特点做一些说明:

(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速 度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线 程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是 线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引 入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合 可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历 LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以 指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用 TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则 会在运行时抛出java.lang.ClassCastException类型的异常。

HashMap是Java的Map家族中一个普通成员,鉴于它可以满足大多数场景的使用条 件,所以是使用频度最高的一个。下文我们主要结合源码,从存储结构、常用方法分析、扩容以及安全性等方面深入 讲解HashMap的工作原理。

内部实现

搞清楚HashMap,首先需要知道HashMap是什么,即它的存储结构­字段;其次弄明白它能干什么,即它的功能实现方法。下面我们针对这两个方面详细展开讲解。

存储结构­ - 字段

从结构实现来讲,HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。

这里需要讲明白两个问题:数据底层具体存储的是什么?这样的存储方式有什么优点呢?

(1) 从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。我们来看Node[JDK1.8]是何物

static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K, V> next;Node(int hash, K key, V value, Node<K, V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}//.......
}

Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是 一个Node对象。

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

例如程序执行下面代码: map.put("美团","小美"); 系统将调用"美团"这个key的hashCode()方法得到其hashCode 值(该方法适用于每个Java对象),然后再通过Hash 算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对的存储位置,有时两个key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀,Hash碰撞的概率就越小,map的存取效率就会越高。 如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现 较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得Hash碰撞的概率又小,哈希桶数组 (Node[] table)占用空间又少呢?答案就是好的Hash算法和扩容机制。 在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码可知,构造函 数就是对下面几个字段进行初始化,源码如下:

int threshold;final float loadFactor;transient int size;transient int modCount;

首先,Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是 HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。 结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过 这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率 的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求 很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子 loadFactor的值,这个值可以大于1。 size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳最大键值对数 量threshold的区别。而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。 强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构 变化。 在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设 计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,具体证明可以参考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小为11,就是桶大小设计为素数 的应用(Hashtable扩容后不能保证还是素数)。HashMap采用这种非常规设计,主要是为了在取模和扩容时做优 化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。 这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长, 则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表 长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。本文不再对红黑树展开讨论,想了解更多红黑树数据结构的工作原理可以参考http://blog.csdn.net/v_july_v/article/details/6105630。

功能实现­ - 方法

HashMap的内部功能实现很多,本文主要从根据key获取哈希桶数组索引位置、put方法的详细执行、扩容过程三个具有代表性的点深入展开讲解。

1. 确定哈希桶数组索引位置

不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只 有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链 表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现(方 法一+方法二):

这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。 对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。 我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的 消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象应该保存在table数组的哪个索引处。 这个方法非常巧妙,它通过h & (table.length ­1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次 方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length­1)运算等价于对length取模,也就是 h%length,但是&比%具有更高的效率。 在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑 到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

下面举例说明下,n为table的长度。

2. 分析HashMap的put方法

HashMap的put方法执行过程可以通过下图来理解,自己有兴趣可以去对比源码更清楚地研究学习

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为 空,转向③; ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以 及equals; ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤; ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表 的插入操作;遍历过程中若发现key已经存在直接覆盖value即可; ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

3. 扩容机制

扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个 新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

==============

转载于:https://my.oschina.net/xinxingegeya/blog/3014550

Java8 HashMap相关推荐

  1. Java8 HashMap详解

    文章推荐 精选java等全套学习资源 精选java电子图书资源 精选大数据学习资源 java项目练习精选 ###Java8 HashMap Java8 对 HashMap 进行了一些修改,最大的不同就 ...

  2. Java8 HashMap 扩容之后旧元素存放位置

    Java8 HashMap 扩容之后旧元素存放位置 根据源码可看出,java 在扩容的时候会新建一个新的 Node<K,V>[] 来存放扩容之后的值,并将旧有的Node数组置空: 至于旧有 ...

  3. Java8 HashMap实现原理探究

    2019独角兽企业重金招聘Python工程师标准>>> 前言:Java8之后新增挺多新东西,在网上找了些相关资料,关于HashMap在自己被血虐之后痛定思痛决定整理一下相关知识方便自 ...

  4. Java8 HashMap源码分析

    前言 今天,我们主要来研究一下在Java8中HashMap的数据结构及一些重要方法的具体实现.       研究HashMap的源代码之前,我们首先来研究一下常用的三种数据结构:数组.链表和红黑树. ...

  5. Java8 HashMap之tableSizeFor

    Java8对许多内置的容器进行了优化与拓展,其中对HashMap的改变尤其大.之后将进行总结. 最近在看HashMap的源码时,发现了里面好多很不错的算法,相比Java7从性能上提高了许多.其中tab ...

  6. java8 hashmap 死循环_踩坑了,JDK8中HashMap依然会死循环!

    作者:Aaron_涛 原文:blog.csdn.net/qq_33330687/article/details/101479385 是否你听说过JDK8之后HashMap已经解决的扩容死循环的问题,虽 ...

  7. Java8 HashMap 扩容机制与线程安全分析

    如果大家有仔细阅读过 HashMap 的源码就会发现 HashMap 的哈希表初始化并不是在其构造函数中进行的,而是 resize() 方法. 这篇文章不对 HashMap 中的树进行介绍. 一.Ha ...

  8. 【Java源码分析】Java8的HashMap源码分析

    Java8中的HashMap源码分析 源码分析 HashMap的定义 字段属性 构造函数 hash函数 comparableClassFor,compareComparables函数 tableSiz ...

  9. Java7/8 中的 HashMap 和 ConcurrentHashMap

    Java7 HashMap  数组+链表 Java7 ConcurrentHashMap   Segment数组+HashEntry数组链表+ReenTrantLock分段锁 Java8 HashMa ...

最新文章

  1. 用友 传入的 json 格式无效_用友网络股吧:被错杀的半导体材料龙头,全年或60%高增长,刚刚走出黄金坑...
  2. 深度揭秘垃圾回收底层,这次让你彻底弄懂她
  3. phpstudy php日志,phpstudy开启网站Apache日志并且按照日期划分创建
  4. SVG基本形状及样式设置
  5. 人工智能时代,为什么很多人都看好Python的发展?
  6. Spring AOP之ThrowsAdvice
  7. php如何获取百度快照,PHP获取某网站的百度快照日期方法
  8. 编程实现基于二维易位置换机制进行信息加解密_基于TEE的TBOX安全技术
  9. java 鲜为人知的知识点_鲜为人知的Java 8功能:广义目标类型推断
  10. C语言递归分形实验-曼德勃罗集
  11. c++ win32 获取串口高低电平_串口和USB的区别,几种常见的串口协议
  12. Git最基本入门,只是个感想总结啊啊啊不要搜到我0.0
  13. sp2 xp 英文版序列号_64位 Windows XP SP2 VOL+简体中文语言包+序列号
  14. java getbytes 乱码_深入解析java String中getBytes()的编码问题
  15. 一步一个脚印——开启博客
  16. python模拟生成微软序列号,python生成随机序列号
  17. C++程序设计的技巧-Pimple的使用
  18. 多进程与多线程的区别,和用途
  19. vue form表单提交动态数据
  20. spm + host

热门文章

  1. 刷脸,带来了方便,也带来了隐患,不可不防呀!
  2. 综述:NLP中的深度学习优势
  3. 谷歌、DeepMind强强联手再发布Dreamer:性能远超“前辈”PlaNet
  4. SAP SD基础知识之免费订单
  5. 《百面机器学习》笔记-特征工程相关面试题
  6. 芯片技术从未止步 助力AI安防迈向新阶段
  7. 解读丨从自动驾驶到学习机器学习:科技发展的15大趋势
  8. Python 之 Numpy (六)分割
  9. (完全解决)Precision and F-score are ill-defined for being 0.0 in labels with no predicted samples.
  10. 瞭望丨光子芯片能否让中国“换道超车”