键值之道:深入学习Java中强大的HashMap(一)

简介: 键值之道:深入学习Java中强大的HashMap

HashMap 基于哈希表的 Map 接口实现,主要用来存放键值对数据HashMap 不是同步的,这意味着它不是线程安全的。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用ConcurrentHashMap。HashMap 最多允许一条数据的键为 null,允许多条数据的值为 null。此外,HashMap 中的映射不是有序的

JDK1.8 之前 HashMap 由数组 + 链表组成,数组是 HashMap 的主体,链表则是主要使用链地址法解决哈希冲突,即当两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用了。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。

HashMap 继承自 AbstractMap 抽象类,并实现了 Map、Cloneable、Serializable,可以用来存储键值对类型的数据,同时支持复制和序列化。

父类 AbstractMap

AbstractMap 提供了一套基于键值对访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问键值对数据的接口。

**AbstractMap 只有一个抽象方法 entrySet(),其他大多方法都是调用 entrySet() 去处理数据。**所以子类继承可以把关注点放在 entrySet() 方法上,这个方法返回一个保存所有 key-value 映射的 Set。

public abstract Set<Entry<K,V>> entrySet();

如果想要实现一个可变的 Map,我们还需要重写 put() 方法,因为 AbstractMap 类中默认不支持 put 实现,子类必须重写该方法的实现,否则会抛出异常:

public V put(K key, V value) {
  throw new UnsupportedOperationException();
}

成员变量

初始化容量:16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  1. 当我们根据 key 的 hash 确定其在数组的位置时,如果 n 为 2 的幂次方,可以保证数据的均匀插入,如果 n 不是 2 的幂次方,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大 hash 冲突,降低 HashMap 的性能
  2. 一般我们可能会想通过 % 求余来确定位置,只不过性能不如 & 运算。所以 HashMap 使用后者来确定位置,而且当 n 是 2 的幂次方时:hash & (length - 1) == hash % length
  3. HashMap 容量为 2 次幂的原因为了数据的的均匀分布,减少冲突,毕竟 hash 冲突越大,代表数组中一个链的长度越大,这样的话会降低性能
  4. 如果创建 HashMap 对象时,输入的数组长度是 10(不是 2 的幂),HashMap 通过一系列位移运算和或运算得到离初始容量最近的 2 的幂次数

最大容量:2 的 30 次幂

static final int MAXIMUM_CAPACITY = 1 << 30;

HashMap 集合最大容量的上限是:2 的 30 次幂。

默认的负载因子: 0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;

链表和树相互转换阈值

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;

JDK1.8 新增特性,当桶中的链表的元素个数超过 8 则会转红黑树。当桶上的结点数小于 6 时红黑树又会转换成链表。

首先我们要知道,无论是链表的结构还是红黑树的结构,都是设计者在寻找一种时间和空间的平衡。链表的查找效率为 O(n),而红黑树的查找效率为 O(logn),查找效率是变高了,但是插入效率变低了,如果从一开始就用红黑树并不合适。从概率学的角度选了一个合适的临界值为 8。

当元素数量为 8 的时候,红黑树的平均查找长度是 3。而链表的平均查找长度是 4,显然树要快一些,而当元素数量小于等于 6 的时候,链表的平均查找长度是 3,其实也很快,没有必要使用红黑树结构,因为转换的过程也需要一定的时间。

6 和 8 中间有个差值 7 还可以防止链表和树之间频繁的转换。

最小树形化阈值:64

static final int MIN_TREEIFY_CAPACITY = 64;

当 HashMap 桶的容量超过这个值时才能进行树形化 ,否则会先选择扩容,而不是树形化。为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD。

存储元素的数组

transient Node<K,V>[] table;

在 JDK1.8 中我们了解到 HashMap 是由数组加链表加红黑树来组成的结构,其中 table 就是 HashMap 中的数组,JDK1.8 之前数组类型是 Entry 类型。从 JDK1.8 之后是 Node 类型。只是换了个名字,都实现了一样的接口:Map.Entry。负责存储键值对数据的。

存放元素的个数

transient int size;

存放元素(键值对)的个数,注意这个不等于数组的长度。

统计修改次数

transient int modCount;

每次扩容和更改 map 结构的计数器。

扩容临界值用来调整大小

int threshold;

计算公式:capacity(数组长度默认16)* loadFactor(负载因子默认 0.75)。这个值是当前已占用数组长度的最大值。当 size >= threshold 的时候,就要考虑对数组进行扩容操作,临界值是衡量数组是否需要扩增的一个标准,扩容后的 HashMap 容量是之前容量的两倍。

哈希表的加载因子:0.75

final float loadFactor;

用来衡量 HashMap 满的程度,表示 HashMap 的疏密程度。loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。计算 HashMap 的实时加载因子:size / capacity。

当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap 太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建 HashMap 集合对象时指定初始容量来尽量避免。

同时在 HashMap 的构造器中可以定制 loadFactor。

/**
 * 构造一个带指定初始容量和加载因子的空 HashMap
 */
public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
                                       initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
                                       loadFactor);
  this.loadFactor = loadFactor;
  this.threshold = tableSizeFor(initialCapacity);
}
为什么加载因子设置为 0.75,初始化临界值是 12?

loadFactor 越趋近于 1,那么数组中存放的数据也就越多,也就越密,会让链表的长度增加,降低查询效率,loadFactor 越趋近于 0,数组中存放的数据也就越少,也就越稀疏。既要兼顾数组利用率还要考虑链表元素不要太多,经过大量测试 0.75 是最佳方案。

threshold 表示扩容临界值,计算公式:capacity(数组长度默认16)* loadFactor(负载因子默认 0.75)。当 size >= threshold 的时候,就要考虑对数组进行扩容操作。所以最开始的初始化临界值是 12,临界值是衡量数组是否需要扩增的一个标准,扩容后的 HashMap 容量是之前容量的两倍。那么接下来的临界值就是 24、48、96 …

构造方法

HashMap 的构造方法主要是用于设置初始化容量和加载因子,也可以传入 Map 对数据初始化。

无参构造

public HashMap() {
  this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

使用无参构造的时候,加载因子 DEFAULT_LOAD_FACTOR 默认值为 0.75f,初始化容量 DEFAULT_INITIAL_CAPACITY 为 16。

指定初始化容量和加载因子

public HashMap(int initialCapacity, float loadFactor) {
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " +
                                       initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " +
                                       loadFactor);
  this.loadFactor = loadFactor;
  this.threshold = tableSizeFor(initialCapacity);
}

默认情况下 HashMap 的容量是 16,但是,如果用户通过构造函数指定了一个数字作为容量,那么会选择大于该数字的第一个 2 的幂作为容量(3->4、7->8、9->16),即 tableSizeFor 方法的作用。

指定初始化容量

public HashMap(int initialCapacity) {
  this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

本质上使用的还是上门的构造方法,只不过是指定初始化容量,使用默认的加载因子。

传入 Map 数据进行初始化

public HashMap(Map<? extends K, ? extends V> m) {
  this.loadFactor = DEFAULT_LOAD_FACTOR;
  putMapEntries(m, false);
}

使用默认的加载因子,之后调用 putMapEntries 处理元素,循环原始 Map,不断调用 putVal 方法添加数据。

成员方法

新增元素 put

public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

HashMap 暴露 put 方法用于添加数据,而 put 方法种实际上调用 putVal 实现添加逻辑。

首先 putVal 方法接收的第一个参数为 key 的哈希值,计算逻辑如下:

static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 如果 key 等于 null,那么返回的哈希值为 0
  • 如果 key 不等于 null,首先调用 key 的 hashCode 方法赋值给 h,然后和 h 无符号右移 16 位后的二进制进行按位异或得到最后的 hash 值

所以从 hash 方法中可以看出,HashMap 允许其中一条数据的键为空。按位异或运算(^):相同的二进制数位上,数字相同,结果为 0,不同为 1。


键值之道:深入学习Java中强大的HashMap(二)https://developer.aliyun.com/article/1480892

相关文章
|
1月前
|
Java
Java之HashMap详解
本文介绍了Java中HashMap的源码实现(基于JDK 1.8)。HashMap是基于哈希表的Map接口实现,允许空值和空键,不同步且线程不安全。文章详细解析了HashMap的数据结构、主要方法(如初始化、put、get、resize等)的实现,以及树化和反树化的机制。此外,还对比了JDK 7和JDK 8中HashMap的主要差异,并提供了使用HashMap时的一些注意事项。
Java之HashMap详解
|
2月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
48 1
|
2月前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
77 2
|
2月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
71 2
|
2月前
|
XML Java 编译器
Java学习十六—掌握注解:让编程更简单
Java 注解(Annotation)是一种特殊的语法结构,可以在代码中嵌入元数据。它们不直接影响代码的运行,但可以通过工具和框架提供额外的信息,帮助在编译、部署或运行时进行处理。
97 43
Java学习十六—掌握注解:让编程更简单
|
1月前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
1月前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
2月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
64 5
|
2月前
|
存储 Java API
详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
【10月更文挑战第19天】深入剖析Java Map:不仅是高效存储键值对的数据结构,更是展现设计艺术的典范。本文从基本概念、设计艺术和使用技巧三个方面,详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
66 3
|
2月前
|
存储 缓存 安全
在Java的Map家族中,HashMap和TreeMap各具特色
【10月更文挑战第19天】在Java的Map家族中,HashMap和TreeMap各具特色。HashMap基于哈希表实现,提供O(1)时间复杂度的高效操作,适合性能要求高的场景;TreeMap基于红黑树,提供O(log n)时间复杂度的有序操作,适合需要排序和范围查询的场景。两者在不同需求下各有优势,选择时需根据具体应用场景权衡。
34 2