[Map]-2-HashMap中傻傻分不清楚的那些概念

本文深入解析HashMap中的核心成员变量:size、loadFactor、threshold及capacity等,并通过代码演示HashMap的扩容机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap中重要的成员变量

先来看一下,HashMap中都定义了哪些成员变量。


上面是一张HashMap中主要的成员变量的图,其中有一个是我们本文主要关注的: sizeloadFactorthresholdDEFAULT_LOAD_FACTORDEFAULT_INITIAL_CAPACITY

我们先来简单解释一下这些参数的含义,然后再分析他们的作用。

HashMap类中有以下主要成员变量:

  • transient int size; 

    • 记录了Map中KV对的个数

  • loadFactor 

    • 装载印子,用来衡量HashMap满的程度。loadFactor的默认值为0.75f(static final float DEFAULT_LOAD_FACTOR = 0.75f;)。

  • int threshold; 

    • 临界值,当实际KV个数超过threshold时,HashMap会将容量扩容,threshold=容量*加载因子

  • 除了以上这些重要成员变量外,HashMap中还有一个和他们紧密相关的概念:capacity 

    • 容量,如果不指定,默认容量是16(static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;)

可能看完了你还是有点蒙,size和capacity之间有啥关系?为啥要定义这两个变量。loadFactor和threshold又是干啥的?

size 和 capacity

HashMap中的size和capacity之间的区别其实解释起来也挺简单的。我们知道,HashMap就像一个“桶”,那么capacity就是这个桶“当前”最多可以装多少元素,而size表示这个桶已经装了多少元素。来看下以下代码:

Map<String, String> map = new HashMap<String, String>();
map.put("hollis""hollischuang");

Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));

Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));

我们定义了一个新的HashMap,并想其中put了一个元素,然后通过反射的方式打印capacity和size。输出结果为:capacity : 16、size : 1

默认情况下,一个HashMap的容量(capacity)是16,设计成16的好处我在《全网把Map中的hash()分析的最透彻的文章,别无二家。》中也简单介绍过,主要是可以使用按位与替代取模来提升hash的效率。

为什么我刚刚说capacity就是这个桶“当前”最多可以装多少元素呢?当前怎么理解呢。其实,HashMap是具有扩容机制的。在一个HashMap第一次初始化的时候,默认情况下他的容量是16,当达到扩容条件的时候,就需要进行扩容了,会从16扩容成32。

我们知道,HashMap的重载的构造函数中,有一个是支持传入initialCapacity的,那么我们尝试着设置一下,看结果如何。

Map<String, String> map = new HashMap<String, String>(1);
map.put("hahaha""hollischuang");

Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));

Map<String, String> map = new HashMap<String, String>(7);
map.put("hahaha""hollischuang");

Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));


Map<String, String> map = new HashMap<String, String>(9);
map.put("hahaha""hollischuang");

Class<?> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));

分别执行以上3段代码,分别输出:capacity : 2、capacity : 8、capacity : 16

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

这里有一个小建议:在初始化HashMap的时候,应该尽量指定其大小。尤其是当你已知map中存放的元素个数时。(《阿里巴巴Java开发规约》)

loadFactor 和 threshold

前面我们提到过,HashMap有扩容机制,就是当达到扩容条件时会进行扩容,从16扩容到32、64、128...

那么,这个扩容条件指的是什么呢?

其实,HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。

在HashMap中,threshold = loadFactor * capacity。

loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,设置成0.75有一个好处,那就是0.75正好是3/4,而capacity又是2的幂。所以,两个数的乘积都是整数(capacity为2也同样)。

对于一个默认的HashMap来说,默认情况下,当其size大于12(16*0.75)时就会触发扩容。

验证代码如下:

Map<String, String> map = new HashMap<>();
map.put("hollis1""hollischuang");
map.put("hollis2""hollischuang");
map.put("hollis3""hollischuang");
map.put("hollis4""hollischuang");
map.put("hollis5""hollischuang");
map.put("hollis6""hollischuang");
map.put("hollis7""hollischuang");
map.put("hollis8""hollischuang");
map.put("hollis9""hollischuang");
map.put("hollis10""hollischuang");
map.put("hollis11""hollischuang");
map.put("hollis12""hollischuang");
Class<?> mapType = map.getClass();

Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));

Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));

Field threshold = mapType.getDeclaredField("threshold");
threshold.setAccessible(true);
System.out.println("threshold : " + threshold.get(map));

Field loadFactor = mapType.getDeclaredField("loadFactor");
loadFactor.setAccessible(true);
System.out.println("loadFactor : " + loadFactor.get(map));

map.put("hollis13""hollischuang");
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));

Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));

Field threshold = mapType.getDeclaredField("threshold");
threshold.setAccessible(true);
System.out.println("threshold : " + threshold.get(map));

Field loadFactor = mapType.getDeclaredField("loadFactor");
loadFactor.setAccessible(true);
System.out.println("loadFactor : " + loadFactor.get(map));

输出结果:

capacity 16
size 12
threshold 12
loadFactor 0.75

capacity 32
size 13
threshold 24
loadFactor 0.75

当HashMap中的元素个数达到13的时候,capacity就从16扩容到32了。

HashMap中还提供了一个支持传入initialCapacity,loadFactor两个参数的方法,来初始化容量和装载因子。不过,一般不建议修改loadFactor的值。

总结

HashMap中size表示当前共有多少个KV对,capacity表示当前HashMap的容量是多少,默认值是16,每次扩容都是成倍的。loadFactor是装载因子,当Map中元素个数超过loadFactor* capacity的值时,会触发扩容。loadFactor* capacity可以用threshold表示。

PS:文中分析基于JDK1.8.0_73

转自Hollis公众号

<think>嗯,用户想了解Java中Map的各种方法。他们说自己不是很了解,所以可能需要一个比较基础的解释。首先,我应该从Map的基本概念开始,说明它是什么,用途是什么。然后,再分常用方法、视图方法和其他重要方法几个部分来详细说明。 记得要举一些例子,这样用户更容易理解。比如,put、get、remove这些基本操作,用例子展示参数和返回值。可能还要提到不同Map实现类的区别,比如HashMap和TreeMap在方法上的不同,比如TreeMap的firstKey方法。 另外,用户可能对视图方法不太熟悉,比如keySet、values、entrySet,需要解释清楚这些返回的是什么,怎么使用。比如用entrySet遍历Map的时候,可以用Map.Entry来获取键值对。 还要注意线程安全的问题,比如ConcurrentHashMap和Hashtable的区别,但用户的问题可能更关注方法本身,这部分可以稍微提一下,或者放在注意事项里。 需要检查有没有遗漏的重要方法,比如compute、merge这些Java 8新增的方法,可能对用户也有帮助,但如果是基础介绍,可能需要放在后面或者单独提一下。或者用户可能只需要常用方法,所以先重点介绍基础方法,再补充高级方法。 还有,要确保术语翻译准确,比如“entrySet”翻译成“条目集合”或者直接保留英文,但例子中可能需要用中文说明。比如“Map.Entry”可以解释为“键值对对象”。 可能用户在实际使用中会遇到问题,比如遍历时修改Map导致的异常,可以提醒使用迭代器的remove方法。或者put方法覆盖旧值的情况,需要说明返回值是旧值。 结构方面,先介绍Map接口,然后分点列出方法,每个方法给出功能、参数、返回值、示例。最后可以总结不同实现类的特性,以及使用注意事项。 需要确保每个方法的解释清晰,没有技术错误。比如,get方法如果键不存在返回null,但如果有null值的情况,可能需要进一步说明containsKey的使用。 可能用户对视图方法的理解有困难,需要举例说明如何使用keySet转换为Set,或者用entrySet来遍历。比如: for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); // 处理键值对 } 这样用户能明白如何应用这些方法。 另外,Java版本的问题,比如replaceAll是Java 8引入的,如果用户使用的是旧版本可能不支持,但可能不需要特别说明,除非用户问到兼容性。 最后,检查有没有错别字,语法是否正确,确保用中文表达准确,例子中的代码正确,比如使用正确的泛型语法,方法调用正确。 总结一下,回答的结构应该是: 1. Map概述 2. 常用方法(put, get, remove, containsKey等) 3. 视图方法(keySet, values, entrySet) 4. 其他重要方法(size, clear, putAll等) 5. 不同实现类的特有方法(如TreeMap的firstKey) 6. 注意事项(线程安全、遍历修改等) 7. 示例代码 这样用户能够逐步理解Map的方法和使用场景。</think>在 Java 中,`Map` 是一个存储键值对(Key-Value)的接口,常用于通过键快速查找值。以下是其核心方法及使用场景的详细说明: --- ### 一、基础操作方法 1. **`V put(K key, V value)`** - **功能**:添加键值对。若键已存在,则覆盖旧值并返回旧值;若键不存在,返回 `null` - **示例**: ```java Map<String, Integer> map = new HashMap<>(); map.put("apple", 10); // 返回 null(首次添加) map.put("apple", 20); // 返回 10(覆盖旧值) ``` 2. **`V get(Object key)`** - **功能**:通过键获取值。若键不存在,返回 `null` - **注意**:需配合 `containsKey` 区分「键不存在」和「键对应值为 `null`」的情况 - **示例**: ```java Integer count = map.get("apple"); // 返回 20 ``` 3. **`V remove(Object key)`** - **功能**:删除键值对。成功删除返回被删的值,否则返回 `null` - **示例**: ```java map.remove("apple"); // 返回 20 ``` 4. **`boolean containsKey(Object key)`** - **功能**:判断是否包含指定键 - **示例**: ```java if (map.containsKey("apple")) { ... } ``` --- ### 二、视图方法(集合视角) 1. **`Set<K> keySet()`** - **功能**:返回所有键组成的 `Set` 集合 - **用途**:遍历所有键或转换为其他集合 - **示例**: ```java for (String key : map.keySet()) { ... } ``` 2. **`Collection<V> values()`** - **功能**:返回所有值组成的 `Collection` 集合 - **注意**:值可能有重复,集合无序 - **示例**: ```java List<Integer> valueList = new ArrayList<>(map.values()); ``` 3. **`Set<Map.Entry<K, V>> entrySet()`** - **功能**:返回所有键值对组成的 `Set`(每个元素是 `Map.Entry` 对象) - **高效遍历方式**: ```java for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); } ``` --- ### 三、其他实用方法 1. **`int size()`** - **功能**:返回键值对数量 - **示例**: ```java if (map.size() > 100) { ... } ``` 2. **`void clear()`** - **功能**:清空所有键值对 - **示例**: ```java map.clear(); ``` 3. **`void putAll(Map<? extends K, ? extends V> m)`** - **功能**:合并另一个 Map 的所有键值对 - **示例**: ```java Map<String, Integer> otherMap = new HashMap<>(); map.putAll(otherMap); ``` --- ### 四、Java 8 新增方法 1. **`V getOrDefault(Object key, V defaultValue)`** - **功能**:安全获取值,若键不存在则返回默认值 - **示例**: ```java int count = map.getOrDefault("banana", 0); // 不存在则返回 0 ``` 2. **`V compute(K key, BiFunction<K, V, V> remappingFunction)`** - **功能**:根据旧值动态计算新值(如累加) - **示例**: ```java map.compute("apple", (k, v) -> v == null ? 1 : v + 1); ``` --- ### 五、不同实现类的特有方法 - **`TreeMap`** 有序 Map 的特有方法: ```java TreeMap<Integer, String> treeMap = new TreeMap<>(); Integer firstKey = treeMap.firstKey(); // 获取最小键 Integer lastKey = treeMap.lastKey(); // 获取最大键 ``` - **`ConcurrentHashMap`** 线程安全方法: ```java concurrentMap.putIfAbsent("key", "value"); // 原子性添加 ``` --- ### 六、注意事项 1. **键的唯一性**:同一 Map 中键不可重复,值可以重复 2. **允许 `null` 值**:`HashMap` 允许键和值为 `null`,但 `TreeMap` 不允许 3. **遍历时修改**:直接删除元素会抛出异常,应使用迭代器的 `remove()` 4. **线程安全**:`Hashtable` 和 `ConcurrentHashMap` 是线程安全实现 --- ### 完整示例 ```java Map<String, Integer> inventory = new HashMap<>(); inventory.put("apple", 10); inventory.put("banana", 5); // 遍历 for (Map.Entry<String, Integer> entry : inventory.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } // 合并 Map Map<String, Integer> newStock = Map.of("orange", 8, "apple", 15); inventory.putAll(newStock); // apple 更新为 15,orange 新增 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值