Saul's blog Saul's blog
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)

Saul.J.Wu

立身之本,不在高低。
首页
后端
分布式
前端
更多
分类
标签
归档
友情链接
关于
GitHub (opens new window)
  • Java入门基础

  • Java核心基础

    • 多线程

    • Java常用类

    • 枚举类与注解

    • Java集合

      • Java集合框架概述
      • Collection
      • List
      • ArrayList
      • LinkedList
      • Vector
      • Set
      • HashSet
      • LinkedHashSet
      • TreeSet
      • Map
      • HashMap
        • 前言
        • 概述
        • 存储结构图示
        • 源码中的重要常量
        • JDK7底层
          • 存储结构
          • 添加元素的过程
          • 扩容
        • JDK8底层
          • 存储结构
          • 扩容和结构变化
          • key的注意点
          • 小结
        • 回头看看HashSet
        • 面试题
        • 总结
      • LinkedHashMap
      • TreeMap
      • Hashtable
      • Properties
      • Collections工具类
    • 数据结构与算法

    • 泛型

    • IO流

    • 网络编程

    • 反射

    • 函数式编程

  • 设计模式

  • Web开发

  • SpringBoot

  • 微服务

  • Elasticsearch

  • 运维

  • 后端
  • Java核心基础
  • Java集合
SaulJWu
2020-12-26

HashMap

# 前言

Java 集合可分为Collection 和Map 两种体系:

  • Collection接口:单列数据,定义了存取一组对象的方法的集合
    • List:元素有序、可重复的集合
      • ArrayList、LinkedList、Vector
    • Set:元素无序、不可重复的集合
      • HashSet、LinkedHashSet、TreeSet
  • Map接口:双列数据,保存具有映射关系“key-value对”的集合,也称为键值对。
    • HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

现在我们开始学习Map的实现类之一:HashMap。

# 概述

  • HashMap是Map 接口使用频率最高的实现类。
  • 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
  • 所有的key构成的集合是Set:**无序的、不可重复的。**所以,key所在的类要重写:equals()和hashCode()
  • 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
  • 一个key-value构成一个entry
  • 所有的entry构成的集合是Set:无序的、不可重复的
  • HashMap 判断两个key 相等的标准是:两个key 通过equals() 方法返回true,hashCode值也相等。
  • HashMap判断两个value相等的标准是:两个value 通过equals() 方法返回true。

# 存储结构图示

  • JDK7

image-20201226225334866

  • JDK8

image-20201226225415061

# 源码中的重要常量

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16。
  • MAXIMUM_CAPACITY :HashMap的最大支持容量,2^30。
  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子,0.75。
  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值8,转化为红黑树。
  • UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表。
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量,默认是64。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
  • table:存储元素的数组,总是2的n次幂
  • entrySet:存储具体元素的集
  • size:HashMap中存储的键值对的数量
  • modCount:HashMap扩容和结构改变的次数。
  • threshold:扩容的临界值,等于容量*填充因子,16*0.75=12
  • loadFactor:填充因子,0.75

# JDK7底层

# 存储结构

HashMap的存储结构:JDK 1.8之前

  • HashMap的内部存储结构其实是数组和链表的结合。
  • 当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,
    • 这个长度在哈希表中被称为容量(Capacity),
    • 在这个数组中可以存放元素的位置我们称之为“桶”(bucket),
    • 每个bucket都有自己的索引,
    • 系统可以根据索引快速的查找bucket中的元素。
  • 每个bucket中存储一个元素,即一个Entry对象,
    • 但每一个Entry对象可以带一个引用变量,用于指向下一个元素,
    • 因此,在一个桶中,就有可能生成一个Entry链。而且新添加的元素作为链表的head。

# 添加元素的过程

添加元素的过程:

  • 向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置i。
  • 如果位置i上没有元素,则entry1直接添加成功。
  • 如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。
    • 如果彼此hash值不同,则直接添加成功。
    • 如果hash值不同,继续比较二者是否equals。
      • 如果返回值为true,则使用entry1的value去替换equals为true的entry的value。
      • 如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。

# 扩容

HashMap的扩容

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。

所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

那么HashMap什么时候进行扩容呢?

  • 当HashMap中的元素个数超过数组大小*loadFactor时,就 会 进 行 数 组 扩 容。*
    • 数组大小指的是数组总大小length,不是数组中个数size
    • loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。
    • 也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,
    • 那么当HashMap中元素个数超过16*0.75=12的时候,
      • (这个值就是代码中的threshold值,也叫做临界值)
    • 就把数组的大小扩展为2*16=32,即扩大一倍,
    • 然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,
    • 所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

# JDK8底层

# 存储结构

HashMap的存储结构:JDK 1.8之后

  • HashMap的内部存储结构其实是数组+链表+树的结合。

    • 当实例化一个HashMap时,会初始化initialCapacity和loadFactor,
    • 在put第一对映射关系时,系统会创建一个长度为initialCapacity的Node数组,
    • 这个长度在哈希表中被称为容量(Capacity),
    • 在这个数组中可以存放元素的位置我们称之为“桶”(bucket),
    • 每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
  • 每个bucket中存储一个元素,即一个Node对象,

    • 但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,
      • 每一个TreeNode对象可以有两个叶子结点left和right,
    • 因此,在一个桶中,就有可能生成一个TreeNode树。
      • 而新添加的元素作为链表的last,或树的叶子结点。

# 扩容和结构变化

那么HashMap什么时候进行扩容和树形化呢?

  • 当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容。
    • 数组大小指的是数组总大小length,不是数组中个数size
    • loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。
    • 也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,
    • 那么当HashMap中元素个数超过16*0.75=12的时候,
      • 这个值就是代码中的threshold值,也叫做临界值
    • 就把数组的大小扩展为2*16=32,即扩大一倍,
    • 然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,
    • 所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

当HashMap中的其中一个链的对象个数如果达到了8个,此时如果当前集合的长度capacity没有达到64,那么HashMap会先扩容解决,

如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。

当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

# key的注意点

关于映射关系的key是否可以修改?

不要修改。

映射关系存储到HashMap中会存储key的hash值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的hash值了,因此如果已经put到Map中的映射关系,再修改key的属性,而这个属性又参与hashcode值的计算,那么会导致匹配不上。

就像之前有个例子修改HashSet里面的对象的属性时,remove()就匹配不到,找不到了。

# 小结

  • 1.HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
  • 2.当首次调用map.put()时,再创建长度为16的数组
  • 3.数组为Node类型,在jdk7中称为Entry类型
  • 4.形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
  • 5.当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

# 回头看看HashSet

我们已经知道HashSet底层其实就是HashMap,添加元素时的源码:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
1
2
3

这个元素e,其实就HashMap中的key

  • PRESENT是HashSet内部定义的静态常量。
private static final Object PRESENT = new Object();
1

PRESENT其实只是空的对象,没有实际意义,只是为了防止控指针异常。

# 面试题

谈谈你对HashMap中put/get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?

负载因子值的大小,对HashMap有什么影响

  • 负载因子的大小决定了HashMap的数据密度。
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

# 总结

HashMap的底层原理?

JDK7和JDK8的原理稍微有点不同

JDK7

  • 在实例化以后,底层创建了长度是16的一堆数组Entry[] table。
  • 存放元素时是以key-value对存放,计算key的哈希值,决定Entry数组中的存放位置。
  • 如果此位置上的数据为空,添加成功。
  • 如果此位置上的数据不为空,意味着此位置上存在一个或多个数据(以链表形式存在),比较存放元素的key的哈希值和已经存在的数据的key的哈希值
    • 如果key的哈希值都不相同,则以链表方式添加成功。
    • 如果跟某一个元素的key的哈希值相同,继续比较它们的equals方法。
      • equals()返回false,则以链表方式添加成功。
      • equals()返回true,说明key是一样的,将存放元素的value去覆盖(替换)已经存在的元素的value。

底层结构:数组+链表。

扩容问题:

在添加过程中,会涉及到扩容问题,当当前元素的个数超出临界值和存放的位置不为空时,默认的库容为原来容量的2倍,并会将原有的数据复制过来。

JDK8底层和JKD7差不多,但是有几个点要注意

  • 在实例化以后,底层创建的数组是Node[]
  • 首次调用put方法添加元素时,底层创建长度为16的数组。
  • 存放元素也是以key-value对存放
    • 当数组的某一个索引位置上的元素以链表的形式存在的数据个数大于8
    • 并且当前链表数组的长度大于64时,
    • 此时此索引位置上的数据结构从链表转换为红黑树。

底层结构:数组+链表+红黑树。

帮我改善此页面 (opens new window)
#HashMap#DEFAULT_INITIAL_CAPACITY#DEFAULT_LOAD_FACTOR
上次更新: 2021/11/16, 01:58:01
Map
LinkedHashMap

← Map LinkedHashMap→

最近更新
01
zabbix学习笔记二
02-28
02
zabbix学习笔记一
02-10
03
Linux访问不了github
12-08
更多文章>
Theme by Vdoing | Copyright © 2020-2022 Saul.J.Wu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式