1、简介
HashMap主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java容器之一,是非线程安全的。
HashMap可以存储null的key和value,但null作为key只能有一个,null作为value可以有多个(注意:仅仅只是HashMap可以有为null的key)。
底层数据结构:
JDK 1.7 数组 + 链表JDK 1.8 数组 + (链表 | 红黑树)JDK 1.8 以前,HashMap 的底层结构是由 数组 + 链表 组成的(注意这里的 链表 不是必然出现的,是为了解决 哈希冲突 导致的一系列问题,才会引入 链表),数组是
HashMap的主体,链表主要是为了解决 哈希冲突 而存在的(使用的是“拉链法”,还有 再哈希法 和 开放寻址法 也可以解决 哈希冲突)。JDK 1.8 开始,HashMap 的底层数据结构是由 数组 + 链表 + 红黑树 组成的(注意这里的 链表 和 红黑树 不是必然出现的,是为了解决 哈希冲突 导致的一系列问题,才会引入 链表 和 红黑树),当出现了 哈希冲突时:
- 1、首先判断链表长度是否大于阈值(默认为 8 ),如果不大于,则直接插入到链表中;
- 2、如果大于,会再判断数组长度是否 < 64,如果 < 64,则进行数组扩容,而不是转换成红黑树。如果数组长度 >= 64 则会转换成红黑树。(当树节点数量 < 6 的时候会从红黑树退化成链表)
HashMap 的默认容量为 16,默认负载因子为 0.75,什么是时候扩容是根据负载因子来判断的,比如说现在负载因子是 0.75,容量为 16,0.75 = 3/4,16 * 3/4 = 12,当容量 > 12 时就会进行扩容,扩容为原来的 2 倍。并且,
HashMap总是使用 2 的幂来作为容量的大小。
部分源码:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
// 容量最大值,边界值
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
}
2、底层数据结构分析
2.1 JDK 1.8 之前
JDK 1.8 之前 HashMap 底层是数组 + 链表结合在一起进行使用,也就是链表散列。
HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存在的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓的扰动函数是指 HashMap 的 hash() 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法,换句话说,使用扰动函数可以减少碰撞,避免 hash 冲突。
JDK 1.8 的 hash 方法:
static final int hash(Object key) {
int h;
// 如果 key 为 null 就直接返回 0
// 否则就返回 key 的 哈希值 异或 key 的哈希值无符号右移 16 位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
JDK 1.7 的 hash 方法:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相比于 JDK 1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓拉链法是指:数组和链表相结合。也就是创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加入到链表中即可。
2.2 JDK 1.8 及之后
相比于之前的版本,JDK 1.8 之后在解决哈希冲突上有了较大的变化。
当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin() 方法。这个方法会根据 HashMap 数组的长度来判断是否要转换成红黑树。只有当数组长度 >= 64 时才会进行转换成红黑树操作,以减少搜索时间。否则就只是执行 resize() 方法对数组进行扩容。
treeifyBin() 方法源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组为空,或者数组的长度小于转换成红黑树的长度(默认为 64)
// 就调用 resize() 方法进行数组的扩容,不进行转换红黑树
// 这里的 table 是一个数组,是 HashMap 的核心
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 如果计算出来的索引已经有值了,就进行转换成红黑树操作
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
3、源码分析
3.1 类属性
类的属性源码:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量,2 的 30 次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时,红黑树会退化成链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的 table 的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是 2 的幂次
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改 map 结构的计数器
transient int modCount;
// 临界值(容量 * 负载因子),当实际大小超过临界值时,会进行扩容
int threshold;
// 负载因子
final float loadFactor;
// HashMap 的核心就是一个 Node 类型的数组,源码 396 行
transient Node<K,V>[] table;
}
-
loadFactor负载因子loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,
loadFactor越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。
loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。给定的默认容量为
16,负载因子为0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了16 * 0.75 = 12就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到rehash、复制数据等操作,所以非常消耗性能。 -
threshold 容量阈值
threshold = capacity * loadFactor,当 Size >= threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
Node 节点类源码:
// Node 节点类
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 每个节点的哈希值是由 key 和 value 的哈希值进行 按位异或后 得到的
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
树节点类源码:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父
TreeNode<K,V> left; // 左
TreeNode<K,V> right; // 右
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; // 判断颜色
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 返回根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
3.2 构造方法
构造方法源码(4 个):
/**
* initialCapacity 容量
* loadFactor 负载因子
* 在构造方法中仍然没有进行数组的初始化
*/
public HashMap(int initialCapacity, float loadFactor) {
// 如果容量小于 0,直接抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
// 如果超过规定容量最大值,就直接把容量最大值赋值给初始化容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 如果负载因子小于等于 0 或者 负载因子不是一个 Number 类型的数值
// 抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 进行属性赋值操作
this.loadFactor = loadFactor;
// 寻找比当前容量大而且最接近 2 的幂次的值
this.threshold = tableSizeFor(initialCapacity);
}
/**
* initialCapacity 容量
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 空构造方法
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* m:Map 集合
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
System.out.println(Float.isNaN(1.0f / 1.0f)); // false
System.out.println(Float.isNaN(0.0f / 0.0f)); // true
System.out.println(Float.isNaN(0.0f)); // false
3.3 put 方法
JDK 1.8 put 方法源码
HashMap 只提供了 put 方法用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
流程图(建议和源码配合阅读):
putVal() 方法源码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果数组长度为 null 或者 数组长度为 0
if ((tab = table) == null || (n = tab.length) == 0)
// 进行数组扩容
n = (tab = resize()).length;
// 通过 hash 值计算对应数组位置,如果为 null
if ((p = tab[i = (n - 1) & hash]) == null)
// 直接在对应数组位置插入值
tab[i] = newNode(hash, key, value, null);
// 当对应数组位置不为 null 时,说明有值存在,哈希冲突
else {
Node<K,V> e; K k;
// 将对应数组位置的 hash 和 key 与要插入的值的 hash 和 key 进行比较
// 如果相同,就直接进行覆盖操作(45 行)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断对应数组位置是否是红黑树结构,如果是,则进行红黑树插入操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果不是红黑树结构,则是链表结构
else {
// 链表尾插法
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 将新节点插入到链表尾部
p.next = newNode(hash, key, value, null);
// 如果链表长度 >= 8 - 1(注意:链表长度是从 0 开始计算的)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 进入是否要转红黑树逻辑
treeifyBin(tab, hash);
break;
}
// 在链表遍历的过程中对链表节点依次进行比较,如果 hash 和 key 相同
// 则直接跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 说明在遍历链表的过程中有 hash 和 key 相同的节点
if (e != null) { // existing mapping for key
// 记录旧的 val
V oldValue = e.value;
// 这里的 onlyIfAbsent 是参数,put 方法传过来的值是 false,所以成立
// 为啥这里要搞一个 onlyIfAbsent 来判断一下呢?
// 我的思考:可以自行调整当 key 相同的策略
//(1)如果传过来一个 false 说明当 key 相同时直接进行覆盖
// (2)如果传过来一个 true 说明当 key 相同时不进行覆盖
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 直接返回旧的 val,方法调用结束
return oldValue;
}
}
++modCount;
// 判断数组长度是否大于阈值
if (++size > threshold)
// 进行数组扩容
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
JDK 1.7 put 方法源码
public V put(K key, V value) {
// 如果是第一次插入元素,进行初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 如果 key 为 null,HashMap 允许 key 为 null,但只能有一个
if (key == null)
return putForNullKey(value);
// 通过 key 计算哈希值
int hash = hash(key);
// 通过 hash 值和数组长度来计算对应的桶下标
int i = indexFor(hash, table.length);
// 遍历链表
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果 key 相同就直接覆盖
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 进行添加
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果数组长度大于等于扩容值,同时对应的桶下标有值(存在哈希冲突)
if ((size >= threshold) && (null != table[bucketIndex])) {
// 进行数组扩容,扩容原先的两倍
resize(2 * table.length);
// 重新计算 hash 值
hash = (null != key) ? hash(key) : 0;
// 根据 hash 值计算对应的桶下标
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
// 添加到数组对应的位置
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K, V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
3.4 get 方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
实际上是调用了 getNode() 方法:
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果 hash 值和 key 都相等
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
// 直接返回
return first;
// 如果不等于 null 说明不只一个节点,进行遍历查找
if ((e = first.next) != null) {
// 如果是红黑树结构
if (first instanceof TreeNode)
// 就调用树的获取节点方法
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 遍历链表
do {
// 如果 hash 和 key 相等就说明找到了,直接返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
3.5 resize 方法
进行数组扩容,同时会重新计算所有的桶下标(因为桶下标是根据数组长度来计算的,所以需要发生改变),同时也意味着会有很大的性能损耗。
《阿里巴巴 Java 开发手册》
17.【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例: HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,resize()方法总共会调用 8 次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。
resize 方法源码:
final Node<K,V>[] resize() {
// oldTab 为当前表的哈希桶
Node<K,V>[] oldTab = table;
// 当前哈希桶的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// threshold 为阈值,当前的阈值
int oldThr = threshold;
// 初始化新的容量和新的阈值为 0
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果超过最大边界值,2^30
if (oldCap >= MAXIMUM_CAPACITY) {
// 则设置阈值为 2^31 - 1
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将新容量设为原来的 2 倍,前提是扩容后不能超过边界值,以及必须要大于默认容量 16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 那么新的阈值也扩容为旧的阈值的 2 倍
newThr = oldThr << 1; // double threshold
}
// 如果当前表是空的,意思是构造 HashMap 时调用了有参的构造函数,阈值进行了初始化
else if (oldThr > 0) // initial capacity was placed in threshold
// 新的容量就为旧的阈值
newCap = oldThr;
// 构造 HashMap 时调用了无参的构造函数,阈值也未进行初始化
else { // zero initial threshold signifies using defaults
// 新的容量就为默认容量 16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的阈值为 默认容量(16) * 默认负载因子(0.75)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 对应的是 (oldThr > 0) 这种情况,阈值进行了初始化,修正阈值
if (newThr == 0) {
// 新的容量 * 负载因子
float ft = (float)newCap * loadFactor;
// 如果未越界就不变,越界了就直接赋值 2 ^ 31 - 1
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新的阈值赋值给旧的阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据新容量进行数组扩容
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将 table 的引用指向新的数组
table = newTab;
// 将旧的哈希桶的元素移动的新的哈希桶中,桶下标要重新进行计算
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)
// 调用红黑树的相关方法来解决
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 第三种情况:有哈希冲突,使用了链表来解决
else { // preserve order
// 对这一条链表进行分类,分类为两条
// 分类的目的是为性能提升
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
// 先记录后继节点,避免丢失
next = e.next;
// 当 (e.hash & oldCap) == 0 时候
// hash & (oldCap - 1) == hash & (2 * oldCap - 1)
// 说明在 2 的指数那一位为 0,而计算数组下标又是 n - 1
// 0001 0000 16
// 0000 1111 (16 - 1)
// 0010 0000 32
// 0001 1111 (32 - 1)
if ((e.hash & oldCap) == 0) {
// 如果是第一次插入节点,记录头节点
if (loTail == null)
loHead = e;
// 尾插法
else
loTail.next = e;
loTail = e;
}
// 与上面一样
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// j:原数组的索引位置
// 加上 oldCap 的原因是:
// 它和新数组的索引只是差了一个 1
// 0001 0000 16
// 0000 1111 (16 - 1)
// 说明 hash 值在 16 的时候 1 对应的这一位肯定是为 1 的
// 0010 0000 32
// 0001 1111 (32 - 1)
// 再进行计算索引,发现缺少的是 16 对应的 1
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 返回新数组
return newTab;
}
3.6 treeifyBin 方法
转换成红黑树的方法。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组为空,或者数组的长度小于转换成红黑树的长度(默认为 64)
// 就调用 resize() 方法进行数组的扩容,不进行转换红黑树
// 这里的 table 是一个数组,是 HashMap 的核心
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 如果计算出来的索引已经有值了,就进行转换成红黑树操作
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
3.7 tableSizeFor 方法
寻找接近 2 的幂次方法。
比如说输入一个 15,它会返回 16;输入 17,它会返回 32。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
思考
为什么 JDK 1.8 的 hash 方法那样设计?
为什么计算对应数组下标的时候不用 % 而用 &?
为什么核心 Node 数组 table 用 transient 修饰?
HashMap 在确定一个 key 被存储在 table 的哪个元素中时,是通过 Object.hashCode() 方法获取到对象的哈希值,并将哈希值与桶个数(就是 table 数组的长度)取模来确定的。
Object.hashCode() 方法是一个 native 方法,其实现依赖于 JVM 虚拟机的实现。所以在不同平台,同一个对象的哈希值可能是不同的,这就导致了其保存在 table 中的位置可能是不同的,直接将一个 HashMap 传输过去可能会出错。
所以现有的 HashMap 的序列化做法,是将其中的所有 key 都直接保存,在反序列化时再重新生成一个 HashMap,并将 key 逐个插入。
有兴趣可以去看看 writeObject() (这个方法是序列化的时候调用)和 readObject() (这个方法是反序列化的时候调用)这两个方法,HashMap 重写了这两个方法。
还有个 readResolve() 方法,是反序列化的时候调用,它的调用顺序在 readObjec() 方法,阻止序列化方式破坏单例就是用的这个方法。