引言
JDK 版本 1.8
我为什么要学习 HashMap
HashMap 是 Java 中非常重要的数据结构,在 Java 中几乎无处不在,几乎所有的框架都会用到 HashMap,比如 Spring、MyBatis、Dubbo 等等。HashMap 的重要性不言而喻,所以我有必要对 HashMap 进行深入的学习。
HashMap 概述
特点
- 是一种键值存储的数据结构,它的存储方式是将键和值进行一一映射,通过键来计算出值的存储地址,然后将值存储到该地址中。
- 允许使用任何类型的键和值,包括自定义的类型。
- 在使用过程中,通过键来计算出值的存储地址,然后从该地址中取出值。这种存储方式使得 HashMap 能够快速的存储和检索数据。
- 是一种非线程安全的数据结构,如果多个线程同时对 HashMap 进行操作,可能会导致数据的不一致。
- 允许使用 null 作为键和值,但是建议尽量避免使用 null 作为键,因为如果使用 null 作为键。
- 存储元素的顺序是不确定的,也就是不能通过迭代器对 HashMap 进行遍历来保证元素的顺序。
- 随着数据量的增加,会进行自动扩容,以提供更好的性能。
源码学习
继承关系
- Map<K,V>:是一种键值对的数据结构接口,定义了 Map 对象的基本操作
- AbstractMap<K,V>:主要是提供了一个 Map 接口的默认实现,以及为 Map 接口的一些方法提供了默认的实现
- Serializable:标识当前类的对象可以被序列化
- Cloneable:用于标记当前类可以进行克隆操作,但是让需要调用 Object.clone() 方法进行浅拷贝
创建对象
在平常使用的过程中,我们通常会使用如下方式来创建 HashMap 对象:
import java.util.HashMap;
import java.util.Map;
Map<String, String> map = new HashMap<>();
其中 Map
需要导入 java.util.Map
包,HashMap
需要导入 java.util.HashMap
包。
创建细节
创建一个 Map 的方式通常是使用 new
关键字来创建,回去调用 HashMap 的构造方法,那就从构造方法去看吧:
// 在构造函数中未指定任何内容时使用的荷载系数(哈希表中元素的数量与哈希表的大小之间的比例)扩容的关键因素之一
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 哈希表的负载因子(已存储的键值对数量与哈希表容量之间的比率)扩容的关键因素之一
final float loadFactor;
/**
* 使用默认初始容量 (16) 和默认负载系数 (0.75) 构造一个空的 HashMap
* 也就是:new HashMap<>(16, 0.75f);
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有其他字段默认
}
// 接下来就看看默认的构造方法吧,也是个有参构造
// 最大容量,如果任一构造函数使用参数隐式指定了较高的值,则使用该容量。必须是 2 的幂<= 1<<30 (位移运算:1073741824)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 要调整大小的下一个大小值(容量 * 负载系数)
int threshold;
/**
* 构造一个具有指定初始容量和负载因子的空 HashMap
*
* @param initialCapacity 初始容量
* @param loadFactor 负载系数
* @throws IllegalArgumentException 如果初始容量为负或负载系数为非正
*/
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);
}
// 对于给定的目标容量,返回大小为 2 的幂
static final int tableSizeFor(int cap) {
// 位运算,将 cap - 1 的二进制位全部置为 1
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 如果 cap - 1 是 2 的幂,则返回 cap,否则返回 cap - 1 的二进制位全部置为 1 的值 + 1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
简单使用
增加
- put
这个方法存在与 Map 接口中的 V put(K key, V value);
简单使用
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String age = map.put("age", "18");
将指定值与此映射中的指定键进行关联。如果映射之前包含键的映射,则替换旧值。
源码分析
// put 方法
public V put(K key, V value) {
// 调用 putVal 方法,将 key 和 value 传入
return putVal(hash(key), key, value, false, true);
}
// 在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。(用来存储数据的)
transient Node<K,V>[] table;
// 此 HashMap 在结构上被修改的次数
transient int modCount;
// 此映射中包含的键值映射数
transient int size;
// 要调整大小的下一个大小值(容量 * 负载系数)
int threshold;
// 使用树而不是列表的箱计数阈值
static final int TREEIFY_THRESHOLD = 8;
// hash 方法,计算 key 的哈希值
static final int hash(Object key) {
int h;
// key 为 null,则返回 0,否则返回 key 的哈希值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 实现 Map.put 和相关方法。
*
* @param hash 键的哈希值
* @param key 键
* @param value 要放置的值
* @param onlyIfAbsent 如果为 true,则不要更改现有值
* @param evict 如果为 false,则表处于创建模式。
* @return 上一个值,如果没有,则为 null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K, V>[] tab; Node<K, V> p; int n, i;
// table 为当前 Map 中的数组,如果为空,则调用 resize() 方法进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
// 初始化数组
n = (tab = resize()).length;
// 计算出数组中的索引
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果索引位置为空,则直接将 key 和 value 封装成 Node 对象,然后放入数组中
tab[i] = newNode(hash, key, value, null);
else {
// 如果索引位置不为空,则进行判断
Node<K, V> e; K k;
// 如果 key 和 hash 值都相同,则直接将 value 覆盖
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
// 将 p 赋值给 e
e = p;
// 如果 p 是 TreeNode 类型,则调用 putTreeVal 方法进行存储
else if (p instanceof TreeNode)
// 通过红黑树的方式进行存储
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else { // 否则就是链表的方式进行存储
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 如果下一个节点为空,则将 key 和 value 封装成 Node 对象,然后放入数组中
if ((e = p.next) == null) {
// 将 key 和 value 封装成 Node 对象,然后放入数组中
p.next = newNode(hash, key, value, null);
// 如果链表长度大于 8,则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 将链表转换为红黑树
treeifyBin(tab, hash);
break;
}
// 如果 key 和 hash 值都相同,则直接将 value 覆盖
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果 e 不为空,则说明 key 和 hash 值都相同(通过上面的操作找到了重复的键),则直接将 value 覆盖
if (e != null) { // existing mapping for key
// 保存旧值(用作返回)
V oldValue = e.value;
// 如果 onlyIfAbsent 为 false,则直接将 value 覆盖(在 put 方法中传的参数,首次使用)
if (!onlyIfAbsent || oldValue == null)
// 将 value 覆盖
e.value = value;
// 在 HashMap 中为空的方法体,主要作用与 LinkedHashMap 中使用
afterNodeAccess(e);
return oldValue;
}
}
// 此 HashMap 在结构上被修改的次数
++modCount;
// 如果 size 大于阈值,则进行扩容
if (++size > threshold)
resize();
// 在 HashMap 中为空的方法体,主要作用与 LinkedHashMap 中使用
afterNodeInsertion(evict);
return null;
}
下面用一张网图再看看吧:(网图)
- putAll
这个方法存在与 Map 接口中的 void putAll(Map<? extends K, ? extends V> m);
简单使用
Map<String, String> map = new HashMap<>();
Map<String, String> map2 = new HashMap<>();
map2.put("name", "张三");
map2.put("age", "18");
map.putAll(map2);
源码分析
/**
* 将指定映射中的所有映射复制到此映射。这些映射将替换此映射对指定映射中当前任何键的任何映射。
*
* @param m 要存储在此映射中的映射
* @throws NullPointerException 如果指定的映射为 null
*/
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
/**
* 实现 Map.putAll 和 Map 构造函数。
*
* @param m 要存储在此映射中的映射
* @param evict 如果为 false,则表处于创建模式。如果为 true,则表已初始化其阈值,因此,如果添加更多元素,则不会调整大小。
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
// 获取 m 的大小
int s = m.size();
// 如果 m 不为空
if (s > 0) {
// 如果 table 为空,则调用 inflateTable 方法进行初始化
if (table == null) { // pre-size
// 获取 m 的负载因子
float ft = ((float)s / loadFactor) + 1.0F;
// 获取 m 的大小,如果 m 的大小大于最大容量,则将阈值设置为最大容量,否则将阈值设置为大于 m 的大小的最小的 2 的幂
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
// 如果阈值大于阈值,则将阈值设置为大于 m 的大小的最小的 2 的幂
if (t > threshold)
// 设置阈值
threshold = tableSizeFor(t);
}
// 如果 m 的大小大于阈值,则进行扩容
else if (s > threshold)
resize();
// 遍历 m
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
// 调用 putVal 方法,将 key 和 value 传入,也就是调用 put 方法
putVal(hash(key), key, value, false, evict);
}
}
}
- putIfAbsent
同样是存在于 Map 接口中的 V putIfAbsent(K key, V value);
相当于(从源码中扒拉出来的):
// 如果不存在 key,则将 key 和 value 封装成 Node 对象,然后放入数组中
V v = map.get(key);
if (v == null)
v = map.put(key, value);
return v;
简单使用
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String age = map.putIfAbsent("age", "18");
源码分析
注意一下和 put 方法的区别:都是调用 putVal 方法,只是在 putVal 方法中的判断条件不同。
第四个参数为 true,表示如果存在 key,则不要更改现有值,如果不存在 key,则进行添加。
// putIfAbsent 方法
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
// put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
删除
- remove(Object key)
这个方法存在与 Map 接口中的 V remove(Object key);
简单使用
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.remove("name");
源码分析
/**
* 从此映射中删除指定键的映射(如果存在)。
*
* @param key 键,其映射将从映射中删除
* @return 与 key 关联的上一个值,如果没有 key 的映射,则为 null。(null 返回还可以指示映射以前将 null 与 key 关联。
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
/**
* 实现 Map.remove 和相关方法。
*
* @param 键的哈希哈希
* @param key 键
* @param value 如果 matchValue,则为要匹配的值,否则忽略
* @param matchValue 如果为 true,则仅在值相等时删除
* @param movable 如果为 false,则在删除时不移动其他节点
* @return 节点,如果没有,则为 null
*/
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 判断 table 是否为空,或者 table 的长度为 0,如果是,则直接返回 null
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 如果 p 的 hash 值和 key 都相同,则直接将 p 赋值给 node
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 第一个节点不相同,则判断是否为红黑树
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 如果是红黑树,则调用红黑树的方法进行查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else { // 如果是链表,则遍历链表
do { // 遍历链表
// 如果 e 的 hash 值和 key 都相同,则直接将 e 赋值给 node
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
// 将 e 的下一个节点赋值给 e
} while ((e = e.next) != null);
}
}
// 如果 node 不为空,则说明找到了要删除的节点,进行删除操作
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
// 如果 node 是 TreeNode 类型,则调用红黑树的方法进行删除
if (node instanceof TreeNode)
// 调用红黑树的方法进行删除
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) // 如果 node 是链表的第一个节点,则将 node 的下一个节点赋值给 tab[index]
tab[index] = node.next;
else // 如果 node 不是链表的第一个节点,则将 node 的下一个节点赋值给 node 的上一个节点的下一个节点
p.next = node.next;
// 此 HashMap 在结构上被修改的次数
++modCount;
// 记录table存储了多少键值对,因为移除了一个,所以此处就减一
--size;
// 在 HashMap 中是空方法,主要是供 LinkedHashMap 使用,因为 LinkedHashMap 重写了该方法
afterNodeRemoval(node);
// 返回 node (被删除的节点)
return node;
}
}
return null;
}
整个流程和 put 方法差不多,也是先判断 table 是否为空,然后判断是否为红黑树,然后判断是否为链表,然后进行删除操作。主要是在删除操作中,如果是红黑树,则调用红黑树的方法进行删除,如果是链表,则进行遍历,找到要删除的节点,然后进行删除操作。
- remove(Object key, Object value)
这个方法存在与 Map 接口中的 boolean remove(Object key, Object value);
简单使用
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
boolean result = map.remove("name", "张三");
相当于:
if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
map.remove(key);
return true;
} else
return false;
源码分析
注意一下和 remove(Object key) 方法的区别:都是调用 removeNode 方法,只是在 removeNode 方法中的判断条件不同。
第四个参数为 true,表示如果存在 key,则不要更改现有值,如果不存在 key,则进行添加。
// remove(Object key, Object value) 方法
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
// remove(Object key) 方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
查询
- get
存在于 Map 接口中的 V get(Object key);
简单使用
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.get("name");
源码分析
// 通过 key 获取 value
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 实现 Map.get 和相关方法。
*
* @param hash 键的哈希值
* @param key 键
* @return 节点,如果没有,则为 null
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断 table 是否为空,或者 table 的长度为 0,如果是,则直接返回 null
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 始终检查第一个节点
// 如果第一个节点的 hash 值和 key 都相同,则直接将 first 赋值给 e
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果第一个节点不相同,则判断是否为红黑树
if ((e = first.next) != null) {
// 如果是红黑树,则调用红黑树的方法进行查找
if (first instanceof TreeNode)
// 调用红黑树的方法进行查找
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do { // 遍历链表
// 如果 e 的 hash 值和 key 都相同,则直接将 e 赋值给 e
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null); // 将 e 的下一个节点赋值给 e
}
}
return null;
}
- getOrDefault
存在于 Map 接口中的 `default V getOrDefault(Object key, V defaultValue)
简单使用
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.getOrDefault("name", "李四");
源码分析
注意和 get 方法的区别:都是调用 getNode 方法,只是在获取到节点后的操作不同。
// getOrDefault 方法
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
// get 方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
常见问题
为什么只能存储一个 null 键
存储最终都会通过计算出键的哈希值,先看看对应的方法
// hash 方法,计算 key 的哈希值
static final int hash(Object key) {
int h;
// key 为 null,则返回 0,否则返回 key 的哈希值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以看出,当键为 null 的时候,返回的哈希值为 0,而哈希值为 0 的时候,存储的位置为数组的第一个位置,所以 HashMap 只能存储一个 null 键。
如何遍历
- 第一种方式:通过 keySet() 方法获取所有的键,然后通过键获取值
对于数据量较小的效率较高,但是对于数据量较大的效率较低,由于需要复制内存,所以效率较低。
Set<String> keySet = map.keySet();
for (String key : keySet) {
System.out.println(key + ":" + map.get(key));
}
- 第二种方式:通过 entrySet() 方法获取所有的键值对,然后通过键值对获取键和值
Set<Map.Entry<String, String>> entrySet = map.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
- 第三种方式:通过 values() 方法获取所有的值
Collection<String> values = map.values();
for (String value : values) {
System.out.println(value);
}
- 第四种方式:通过 forEach() 方法获取所有的键值对,然后通过键值对获取键和值
map.forEach((key, value) -> {
System.out.println(key + ":" + value);
});
- 第五种方式:通过 Iterator 迭代器获取所有的键值对,然后通过键值对获取键和值(和第二种类似)
虽然避免了内存复制,但是在遍历的过程中,如果对 HashMap 进行了修改,则会抛出 ConcurrentModificationException 异常。也不会保证遍历顺序。
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println(entry.getKey() + ":" + entry.getValue());
}
- 第六种方式:通过 Stream 流获取所有的键值对,然后通过键值对获取键和值
在处理数据量较小的情况下效率较低,因为并行处理的开销比较大,但是在处理数据量较大的情况下效率较高。
map.entrySet().forEach(entry -> {
System.out.println(entry.getKey() + ":" + entry.getValue());
});
- 第七种方式:通过 Stream 流获取所有的键值对,然后通过键值对获取键和值(和上面使用的流不同)
map.entrySet().parallelStream().forEach(entry -> {
System.out.println(entry.getKey() + ":" + entry.getValue());
});
那种遍历效率较好
要根据其内部结构来进行分析,HashMap 的内部结构是数组 + 链表 + 红黑树,所以遍历效率较好的应该是:
- 使用迭代器(上述第五种)
- 使用 for-each(上述第二种)
- 使用 forEach 和 lambda 表达式(上述第四种)
- 使用 stream 流(上述第六种)
具体选择那种方法,可以根据实际情况进行选择,如果是在单线程环境下,建议使用 for-each,如果是在多线程环境下,建议使用迭代器。
怎么扩容
在上面的 put 方法中,有这么一段代码:
// 如果 size 大于阈值,则进行扩容
if (++size > threshold)
resize();
也就是说 resize() 方法是进行动态扩容的关键:
// 最大容量,如果任一构造函数使用参数隐式指定了较高的值,则使用该容量。必须是 2 的幂<= 1<<30 (位移运算:1073741824)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始容量(必须是 2 的幂)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 在构造函数中未指定任何内容时使用的荷载系数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 哈希表的负载因子
final float loadFactor;
final Node<K,V>[] resize() {
// 将 table 赋值给 oldTab,对扩容前的数组进行保存
Node<K,V>[] oldTab = table;
// 获取 oldTab 的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取 oldTab 的阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 如果 oldCap 大于 0。也就是说,如果 oldTab 不为空,则进行扩容
if (oldCap > 0) {
// 如果 oldCap 大于最大容量,则将阈值设置为最大容量,并终止扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果 oldCap * 2 小于最大容量,并且 oldCap 大于等于默认容量,则将 newCap 设置为 oldCap * 2,newThr 设置为 oldThr * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 双阈值
}
// 如果 oldThr 大于 0(也就是 threshold 为 0 ),则将 newCap 设置为 oldThr
else if (oldThr > 0) // 初始容量处于阈值
newCap = oldThr;
else { // 零初始阈值表示使用默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// ft 为 newCap * 0.75,如果 ft 小于最大容量,并且 newCap 小于最大容量,则将 newThr 设置为 ft,否则将 newThr 设置为最大容量
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
// 使用 loadeFactor 重新设置阈值,保存在 threshold 中
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建一个新的数组,长度为 newCap
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将新创建的数组赋值给 table,完成 table 的初始化(扩容)
table = newTab;
// 如果 oldTab 不为空(扩容前的数组中存在数据),则进行数据迁移,迁移到扩容后的数组中
if (oldTab != null) {
// 对旧数组进行遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 如果旧数组中当前节点的元素不为空,则进行数据迁移
if ((e = oldTab[j]) != null) {
// 将旧数组中当前节点的元素置为 null,方便垃圾回收
oldTab[j] = null;
// 如果当前节点的下一个节点为空,则直接将当前节点放入新数组中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果当前节点的下一个节点不为空,则需要将当前节点的链表进行拆分,然后再放入新数组中
else if (e instanceof TreeNode)
// 如果当前节点是红黑树,则调用 split 方法进行拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 否则就是链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do { // 遍历链表
// 将当前节点的下一个节点赋值给 next
next = e.next;
// 如果当前节点的 hash 值和 oldCap 相与为 0,则将当前节点放入 loHead 中
if ((e.hash & oldCap) == 0) {
// 如果 loTail 为空,则将 loHead 和 loTail 都设置为当前节点
if (loTail == null)
loHead = e;
// 如果 loTail 不为空,则将 loTail 的下一个节点设置为当前节点
else
loTail.next = e;
loTail = e;
}
else { // 否则就是 oldCap
// 如果 hiTail 为空,则将 hiHead 和 hiTail 都设置为当前节点
if (hiTail == null)
hiHead = e;
// 如果 hiTail 不为空,则将 hiTail 的下一个节点设置为当前节点
else
hiTail.next = e;
// 将 hiTail 设置为当前节点
hiTail = e;
}
} while ((e = next) != null); // 将 next 赋值给 e
// 如果 loTail 不为空,则将 loTail 的下一个节点设置为 null,然后将 loHead 放入新数组中
if (loTail != null) {
// 将 loTail 的下一个节点设置为 null
loTail.next = null;
// 将 loHead 放入新数组中
newTab[j] = loHead;
}
// 如果 hiTail 不为空,则将 hiTail 的下一个节点设置为 null,然后将 hiHead 放入新数组中
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新数组
return newTab;
}
简单概括一下,就是先判断原始数组是否为空,如果为空,则进行初始化,如果不为空,则判断是否需要扩容,如果需要扩容,则进行扩容,扩容后将原始数组的数据再次放入新的数组中,如果不需要扩容,则不进行扩容。
总结
- 高性能:HashMap 在 Java 中提供了一种快速且高效的方式来存储和检索键值对。由于其内部使用了哈希表,因此能够在很短的时间内对数据进行存储和检索等操作。这种性能优势使得其成为存储和检索大量数据的重要工具。
- 键值存储:HashMap 是一种键值存储的数据结构,它的存储方式是将键和值进行一一映射,通过键来计算出值的存储地址,然后将值存储到该地址中。当需要获取值时,通过键来计算出值的存储地址,然后从该地址中取出值。这种存储方式使得 HashMap 能够快速的存储和检索数据。
- 动态扩容:其内部能够动态调整容量,当容量不足时,会自动扩容,以便能够存储更多的数据。当存储的数据量增加时,会自动扩容,以便能够存储更多的数据。当存储的数据量减少时,会自动缩容,以便节省内存空间,而不需要手动进行管理。
- 线程不安全:HashMap 是线程不安全的,如果多个线程同时对 HashMap 进行操作,可能会导致数据的不一致。因此,如果需要在多线程环境下使用 HashMap,需要进行额外的同步处理。因此,在多线程环境下,建议使用线程安全的 Map 进行实现,如:ConcurrentHashMap。
- 易操作:其提供了简单易用的 API,使得存储、检索和删除等操作都变得非常简单,此外还提供了许多实用的方法,如:containsKey()、containsValue()、keySet()、values()、entrySet() 等方法,使得开发人员能够快速的对数据进行操作。