浅析 java 中的 Map.of(...) 方法和 Map.ofEntries(...) 方法
JDK 9 在 java.util.Map 接口中提供了一组 of(...) 静态方法(这里我用 ... 表示任意的参数类型/个数,包括 个参数的情况)以及一个 ofEntries(...) 方法。通过调用这些静态方法,我们可以构造不可变的(immutable) map。
这组 Map.of(...) 方法的参数中,key-value pair 的数量 可以是 中的任意值。当我们需要提供超过 组 key-value pair 时,可以调用 Map.ofEntries(...) 方法。在 这个方法 的 javadoc 里可以找到如下的例子 ⬇️
我把代码复制到下方了 ⬇️
import static java.util.Map.entry;
Map map = Map.ofEntries(
entry(1, \"a\"),
entry(2, \"b\"),
entry(3, \"c\"),
...
entry(26, \"z\"));
当您调用这些方法时,是否想过以下问题? ⬇️
Map.of(...)/Map.ofEntries(...)返回的Map的精确类型是什么?是HashMap吗,还是其他类型的Map?- 为什么
Map.of(...)/Map.ofEntries(...)返回的Map具有不可变(immutable)的特点,它是如何实现的? Map.of(...)/Map.ofEntries(...)所返回的Map,是如何存储元素的?
本文会从以上 个问题入手,对 Map.of(...)/Map.ofEntries(...) 方法进行探索。
要点
-
Map.of(...)所返回的Map的精确类型与参数中key-valuepair的 个数 有关- 如果 (即
key-valuepair的数量为 ),Map的精确类型会是java.util.ImmutableCollections$Map1 - 如果 且 (即
key-valuepair的数量不超过 且不是 ) ,Map的精确类型会是java.util.ImmutableCollections$MapN
- 如果 (即
-
Map.ofEntries(...)所返回的Map的精确类型与参数中Entry的 数量 有关- 如果 (即
Entry的数量为 ),Map的精确类型会是java.util.ImmutableCollections$Map1 - 如果 (即
Entry的数量不是 ),Map的精确类型会是java.util.ImmutableCollections$MapN
- 如果 (即
-
AbstractImmutableMap中的put(...)/remove(...)等方法会抛出UnsupportedOperationException,而Map1和MapN都继承了AbstractImmutableMap,所以Map1/MapN的实例就是不可变(immutable)的了 -
MapN中有table字段,它的length会是key-valuepair的数量的 倍。在保存元素和查询元素时,会使用 开放寻址法(open addressing)
我画了简单的思维导图 ⬇️
mindmap
root(\"Map.of(...)/Map.ofEntries(...)\")
n1(\"只有一个 key-value pair 时\")
n11(\"返回的是 Map1 的实例\")
Map1 继承了 AbstractImmutableMap 所以 Map1 的实例是 immutable 的
Map1 中的 k0 和 v0 字段分别保存了 key 和 value 的值
n2(\"key-value pair 的数量不等于 1 时\")
n21(\"返回的是 MapN 的实例\")
MapN 继承了 AbstractImmutableMap 所以 MapN 的实例是 immutable 的
MapN 中的 table 字段,使用开放寻址法来保存和查询数据
正文
问题一: Map.of(...)/Map.ofEntries(...) 返回的 Map 的精确类型是什么?是 HashMap 吗,还是其他类型的 Map?
我写了如下的代码来进行探索 ⬇️
import java.util.Map;
public class Question1 {
public static void main(String[] args) {
// for Map.of(...)
System.out.println(Map.of().getClass().getName());
System.out.println(Map.of(\'A\', 65).getClass().getName());
System.out.println(Map.of(\'A\', 65, \'B\', \"66\").getClass().getName());
// for Map.ofEntries(...)
System.out.println(Map.ofEntries().getClass().getName());
System.out.println(
Map.ofEntries(
Map.entry(\'A\', 65)
).getClass().getName()
);
System.out.println(
Map.ofEntries(
Map.entry(\'A\', 65),
Map.entry(\'B\', 66)
).getClass().getName()
);
}
}
请将以上代码保存为 Question1.java,用如下的命令可以编译 Question1.java 并运行 Question1 类的 main(...) 方法。
javac Question1.java
java Question1
运行结果如下
java.util.ImmutableCollections$MapN
java.util.ImmutableCollections$Map1
java.util.ImmutableCollections$MapN
java.util.ImmutableCollections$MapN
java.util.ImmutableCollections$Map1
java.util.ImmutableCollections$MapN
看来 Map.of(...)/Map.ofEntries(...) 方法返回的值可以是以下两个类的实例 ⬇️
java.util.ImmutableCollections$Map1java.util.ImmutableCollections$MapN
但仅凭以上几行代码所列举的情况,并不能说明 Map.of(...)/Map.ofEntries(...) 的返回值是否有可能是其他类的实例,我们去看看源码。
Map.of(...) 方法
在 Map.java 中可以找到各个 Map.of(...) 方法的代码。
key-value pair 的个数 共有 种情况。其中 的情况代码高度雷同,以下只展示 的情况 ⬇️ (为了节约空间,对应的 javadoc 都略去了)
@SuppressWarnings(\"unchecked\")
static Map of() {
return (Map) ImmutableCollections.EMPTY_MAP;
}
static Map of(K k1, V v1) {
return new ImmutableCollections.Map1(k1, v1);
}
static Map of(K k1, V v1, K k2, V v2) {
return new ImmutableCollections.MapN(k1, v1, k2, v2);
}
static Map of(K k1, V v1, K k2, V v2, K k3, V v3) {
return new ImmutableCollections.MapN(k1, v1, k2, v2, k3, v3);
}
可以将代码中的信息汇总成下方的表格 ⬇️
key-value pair 的个数 |
Map.of(...) 返回什么 |
说明 |
|---|---|---|
ImmutableCollections.EMPTY_MAP |
⬅️ 它是 ImmutableCollections.MapN 类型的 |
|
ImmutableCollections.Map1 的实例 |
||
ImmutableCollections.MapN 的实例 |
时,Map.of() 返回的也是 java.util.ImmutableCollections$MapN 的实例。所以可以把上方的表格再简化一下,变成下面这样 ⬇️
key-value pair 的个数 |
Map.of(...) 返回什么 |
|---|---|
java.util.ImmutableCollections$Map1 的实例 |
|
| 且 | java.util.ImmutableCollections$MapN 的实例 |
Map.ofEntries(...) 方法
我们直接看 Map.ofEntries(...) 的源码 ⬇️
@SafeVarargs
static Map ofEntries(Entry... entries) {
if (entries.length == 0) { // implicit null check of entries array
@SuppressWarnings(\"unchecked\")
var map = (Map) ImmutableCollections.EMPTY_MAP;
return map;
} else if (entries.length == 1) {
// implicit null check of the array slot
return new ImmutableCollections.Map1(entries[0].getKey(),
entries[0].getValue());
} else {
Object[] kva = new Object[entries.length << 1];
int a = 0;
for (Entry<? extends K, ? extends V> entry : entries) {
// implicit null checks of each array slot
kva[a++] = entry.getKey();
kva[a++] = entry.getValue();
}
return new ImmutableCollections.MapN(kva);
}
}
这个方法内用 if-else 语句对 entries.length 进行了分类处理。具体的情况,我整理成了如下表格 ⬇️
entries.length 的值 |
Map.ofEntries(...) 返回什么 |
说明 |
|---|---|---|
ImmutableCollections.EMPTY_MAP |
⬅️ 它是 ImmutableCollections.MapN 类型的 |
|
ImmutableCollections.Map1 的实例 |
||
ImmutableCollections.MapN 的实例 |
时,Map.ofEntries(...) 返回的也是 java.util.ImmutableCollections$MapN 的实例。所以可以把上方的表格再简化一下,变成下面这样 ⬇️
entries.length 的值 |
Map.ofEntries(...) 返回什么 |
|---|---|
ImmutableCollections.Map1 的实例 |
|
ImmutableCollections.MapN 的实例 |
Map1/MapN 的类图如下(图中把所有的泛型信息都省略了) ⬇️
classDiagram
Map <|.. AbstractMap
AbstractMap <|-- AbstractImmutableMap
Serializable <|.. AbstractImmutableMap
AbstractImmutableMap <|-- Map1
AbstractImmutableMap <|-- MapN
AbstractMap
AbstractImmutableMap
<> Map
<> Serializable
| 在上图中的类名/接口名 | Fully Qualified Name |
|---|---|
AbstractImmutableMap |
java.util.ImmutableCollections$AbstractImmutableMap |
AbstractMap |
java.util.AbstractMap |
Map |
java.util.Map |
Map1 |
java.util.ImmutableCollections$Map1 |
MapN |
java.util.ImmutableCollections$MapN |
Serializable |
java.io.Serializable |
小结
Map.of(...) 所返回的 Map 的精确类型与参数中 key-value pair 的 个数 有关
- 如果 ,
Map的精确类型会是java.util.ImmutableCollections$Map1 - 如果 且 ,
Map的精确类型会是java.util.ImmutableCollections$MapN
Map.ofEntries(...) 所返回的 Map 的精确类型与参数中 Entry 的 数量 有关
- 如果 ,
Map的精确类型会是java.util.ImmutableCollections$Map1 - 如果 ,
Map的精确类型会是java.util.ImmutableCollections$MapN
问题二:为什么 Map.of(...)/Map.ofEntries(...) 返回的 Map 具有不可变(immutable)的特点,它是如何实现的?
我们写些代码来进行分析。 请将以下代码保存为 Question2.java。
import java.util.Map;
public class Question2 {
public static void main(String[] args) {
Map.of().put(\'a\', 97);
}
}
如下的命令可以编译 Question2.java 以及运行 Question2 类中的 main(...) 方法。
javac Question2.java
java Question2
运行上述命令后,会看到以下的报错
Exception in thread \"main\" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:159)
at java.base/java.util.ImmutableCollections$AbstractImmutableMap.put(ImmutableCollections.java:1318)
at Question2.main(Question2.java:5)
根据上述报错信息,可以找到抛出异常的位置如下 ⬇️
从图中的代码可以看出 clear()/putAll(...)/remove(...) 等方法都会抛出 UnsupportedOperationException
我画了类图来辅助理解 ⬇️ (图中将所有泛型信息都省略了,和 问题二 无关的接口/字段/方法也略去了)
classDiagram
class `java.util.Map`
class `java.util.AbstractMap`
class `java.util.ImmutableCollections$AbstractImmutableMap`
class `java.util.ImmutableCollections$Map1`
class `java.util.ImmutableCollections$MapN`
`java.util.Map` <|.. `java.util.AbstractMap`
`java.util.AbstractMap` <|-- `java.util.ImmutableCollections$AbstractImmutableMap`
`java.util.ImmutableCollections$AbstractImmutableMap` <|-- `java.util.ImmutableCollections$Map1`
`java.util.ImmutableCollections$AbstractImmutableMap` <|-- `java.util.ImmutableCollections$MapN`
class `java.util.ImmutableCollections$AbstractImmutableMap` {
clear()
compute(K, java.util.function.BiFunction)
computeIfAbsent(K, java.util.function.Function);
computeIfPresent(K, java.util.function.BiFunction)
merge(K, V, java.util.function.BiFunction)
put(K, V)
putAll(java.util.Map)
putIfAbsent(K, V)
remove(java.lang.Object)
remove(java.lang.Object, java.lang.Object)
replace(K, V)
replace(K, V, V)
replaceAll(java.util.function.BiFunction)
}
`java.util.AbstractMap`
`java.util.ImmutableCollections$AbstractImmutableMap`
<> `java.util.Map`
note for `java.util.ImmutableCollections$AbstractImmutableMap` \"注意: 在 AbstractImmutableCollection 中展示的 13 个方法都会抛 UnsupportedOperationException\"
小结
AbstractImmutableMap 中的 clear()/putAll(...)/remove(...) 等方法会抛出 UnsupportedOperationException,而 Map1 和 MapN 都继承了 AbstractImmutableMap,所以 Map1/MapN 的实例是不可变的(immutable)。
问题三:Map.of(...)/Map.ofEntries(...) 所返回的 Map,是如何存储元素的?
这里需要区分 Map1/MapN 两种情况
Map1
在 ImmutableCollections.java 中,可以找到 Map1 的代码,其中部分代码如下 ⬇️
// Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap
static final class Map1 extends AbstractImmutableMap implements Serializable {
@Stable
private final K k0;
@Stable
private final V v0;
Map1(K k0, V v0) {
this.k0 = Objects.requireNonNull(k0);
this.v0 = Objects.requireNonNull(v0);
}
@Override
public Set<Map.Entry> entrySet() {
return Set.of(new KeyValueHolder(k0, v0));
}
@Override
public V get(Object o) {
return o.equals(k0) ? v0 : null; // implicit nullcheck of o
}
@Override
public boolean containsKey(Object o) {
return o.equals(k0); // implicit nullcheck of o
}
@Override
public boolean containsValue(Object o) {
return o.equals(v0); // implicit nullcheck of o
}
@Override
public int size() {
return 1;
}
@Override
public boolean isEmpty() {
return false;
}
Map1 的字段
Map1 中有如下两个字段 ⬇️
private final K k0private final V v0
Map1 的构造函数
在调用 Map1(K k0, V v0) 这个构造函数时,
k0参数会被赋给k0字段v0参数会被赋给v0字段
Map1 中的 get(Object)/containsKey(Object)/size()/isEmpty() 等方法的逻辑比较直观,这里就不赘述了。
小结
Map1 可以处理 个 key-value pair 的情况。
MapN
在 ImmutableCollections.java 中,可以找到 MapN 的代码,其中部分代码如下 ⬇️
/**
* An array-based Map implementation. There is a single array \"table\" that
* contains keys and values interleaved: table[0] is kA, table[1] is vA,
* table[2] is kB, table[3] is vB, etc. The table size must be even. It must
* also be strictly larger than the size (the number of key-value pairs contained
* in the map) so that at least one null key is always present.
* @param the key type
* @param the value type
*/
// Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap
static final class MapN extends AbstractImmutableMap implements Serializable {
@Stable
final Object[] table; // pairs of key, value
@Stable
final int size; // number of pairs
MapN(Object... input) {
if ((input.length & 1) != 0) { // implicit nullcheck of input
throw new InternalError(\"length is odd\");
}
size = input.length >> 1;
int len = EXPAND_FACTOR * input.length;
len = (len + 1) & ~1; // ensure table is even length
table = new Object[len];
for (int i = 0; i < input.length; i += 2) {
@SuppressWarnings(\"unchecked\")
K k = Objects.requireNonNull((K)input[i]);
@SuppressWarnings(\"unchecked\")
V v = Objects.requireNonNull((V)input[i+1]);
int idx = probe(k);
if (idx >= 0) {
throw new IllegalArgumentException(\"duplicate key: \" + k);
} else {
int dest = -(idx + 1);
table[dest] = k;
table[dest+1] = v;
}
}
}
@Override
public boolean containsKey(Object o) {
Objects.requireNonNull(o);
return size > 0 && probe(o) >= 0;
}
@Override
public boolean containsValue(Object o) {
Objects.requireNonNull(o);
for (int i = 1; i < table.length; i += 2) {
Object v = table[i];
if (v != null && o.equals(v)) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
int hash = 0;
for (int i = 0; i < table.length; i += 2) {
Object k = table[i];
if (k != null) {
hash += k.hashCode() ^ table[i + 1].hashCode();
}
}
return hash;
}
@Override
@SuppressWarnings(\"unchecked\")
public V get(Object o) {
if (size == 0) {
Objects.requireNonNull(o);
return null;
}
int i = probe(o);
if (i >= 0) {
return (V)table[i+1];
} else {
return null;
}
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
在上面的代码中,可以看到 MapN 中的字段以及构造函数的逻辑。
MapN 的字段
MapN 中有如下两个字段 ⬇️
final Object[] tablefinal int size
而这两个字段的作用是 ⬇️
- 在
MapN的构造函数中,会向table字段填充元素(具体是如何填充的,我们等一下再说),而table字段是保存各个key-valuepair的地方。 size字段用于保存key-valuepair的数量。
MapN 的构造函数
我在构造函数的代码中加了点注释 ⬇️ (代码里原有的注释也没删除)
MapN(Object... input) {
if ((input.length & 1) != 0) { // implicit nullcheck of input
throw new InternalError(\"length is odd\");
}
// size 是 key-value pair 的数量
size = input.length >> 1;
// EXPAND_FACTOR 值为 2,所以 table 的长度会是 key-value pair 的数量的 4 倍
int len = EXPAND_FACTOR * input.length;
len = (len + 1) & ~1; // ensure table is even length
table = new Object[len];
// 遍历处理,为每一个 key-value pair 找到保存它的地方
for (int i = 0; i < input.length; i += 2) {
@SuppressWarnings(\"unchecked\")
K k = Objects.requireNonNull((K)input[i]);
@SuppressWarnings(\"unchecked\")
V v = Objects.requireNonNull((V)input[i+1]);
// 利用开放寻址法(open addressing)找到 k 对应的位置
int idx = probe(k);
if (idx >= 0) {
throw new IllegalArgumentException(\"duplicate key: \" + k);
} else {
// -(idx + 1) 就是 k 应该去的位置,v 会保存在 k 之后的那个位置
int dest = -(idx + 1);
table[dest] = k;
table[dest+1] = v;
}
}
}
这里大部分逻辑都很直观,只有 int idx = probe(k); 这一行看起来不太好懂。
这里涉及 开放寻址法(open addressing)。我在 [Java] 浅析 Set.of(…) 方法 一文中对 开放寻址法 进行了介绍,有兴趣的读者可以看一看,本文就不重复介绍它了。我在 probe(Object) 方法里也加了些注释 ⬇️ (代码里原有的注释也没删除)
// returns index at which the probe key is present; or if absent,
// (-i - 1) where i is location where element should be inserted.
// Callers are relying on this method to perform an implicit nullcheck
// of pk.
private int probe(Object pk) {
// key (即 pk) 对应的下标总是偶数,而 value 会保存在 key 之后的那个位置
int idx = Math.floorMod(pk.hashCode(), table.length >> 1) << 1;
while (true) {
@SuppressWarnings(\"unchecked\")
K ek = (K)table[idx];
if (ek == null) { // 说明 idx 这个下标尚未被占用
return -idx - 1;
} else if (pk.equals(ek)) { // 说明有重复的 key 出现
return idx;
} else if ((idx += 2) == table.length) { // idx 这个下标被其他 key 占据,按照开放寻址法的思路,应该继续检查 (id + 2) 的位置(注意 table 被视为循环数组)
idx = 0;
}
}
}
下面举个例子来说明 MapN 里是如何使用 开放寻址法 来保存 key-value pair 的。
import java.lang.reflect.Field;
import java.util.Map;
public class Question3 {
public static void main(String[] args) throws Exception {
// 随便找个对应的汉字(g->gao 高, w->wen 温, h->hai 海, d->dao 岛)
Map map = Map.of(
\'g\', \"高\",
\'w\', \"温\",
\'h\', \"海\",
\'d\', \"岛\"
);
Field field = map.getClass().getDeclaredField(\"table\");
field.setAccessible(true);
Object[] table = (Object[]) field.get(map);
for (int i = 0; i < table.length; i++) {
if (table[i] != null) {
String message = String.format(\"[%s](%s) is at index: %s\", table[i], table[i].getClass().getSimpleName(), i);
System.out.println(message);
} else {
System.out.println(\"// null is at index: \" + i);
}
}
}
}
请将上方的代码保存为 Question3.java。用下方的命令可以编译 Question3.java 并运行 Question3 里的 main(...) 方法。
javac Question3.java
java --add-opens=java.base/java.util=ALL-UNNAMED Question3
运行结果如下
[w](Character) is at index: 0
[温](String) is at index: 1
[h](Character) is at index: 2
[海](String) is at index: 3
// null is at index: 4
// null is at index: 5
// null is at index: 6
// null is at index: 7
[d](Character) is at index: 8
[岛](String) is at index: 9
// null is at index: 10
// null is at index: 11
// null is at index: 12
// null is at index: 13
[g](Character) is at index: 14
[高](String) is at index: 15
下表展示了下面的 个 key-value pair 保存到 table 字段的过程 ⬇️
\'g\'\"高\"\'w\'\"温\"\'h\'\"海\"\'d\'\"岛\"
保存它之后,table 的内容 |
|
|---|---|
\'g\' \"高\" |
|
\'w\' \"温\" |
|
\'h\' \"海\" |
|
\'d\' \"岛\" |
至于这 个 key-value pair 为什么会保存在表中所展示的位置,下文马上会解释。一开始 table 字段中是 16 个 null 值 ⬇️
保存这 个 key-value pair 时,会用到每个 key 的 hashCode,重要的信息梳理如下 ⬇️
hashCode of key |
hashCode |
key 对应的 idx | key 保存到了哪个 idx? | ||
|---|---|---|---|---|---|
\'g\' |
\"高\" |
||||
\'w\' |
\"温\" |
(因为\'g\' 已经占据了下标为 的位置) |
|||
\'h\' |
\"海\" |
(因为\'w\' 已经占据了下标为 的位置) |
|||
\'d\' |
\"岛\" |
小结
MapN 中有 table 字段,如果我们要在 table 中保存 个 key-value pair,那么 table.length 会是 。根据 key 的 hashCode,可以计算出 key/value 应该保存在什么位置(如果遇到冲突,则使用 开放寻址法 来找到合适的下标)。
其他
展示 table 字段中 key/value 分布的那几张图是怎么画出来的?
前往 mermaid.live,那里可以绘制 block 图。具体的语法可以参考 Block Diagrams Documentation 一文。用以下代码可以画出其中的一张图(其他图的代码也是类似的,就不赘述了)⬇️
block
block
columns 8
s0[\"\'w\'\"] s1[\"\"温\"\"] s2[\"\'h\'\"] s3[\"\"海\"\"]
s4[\"null\"] s5[\"null\"] s6[\"null\"] s7[\"null\"]
i0[\"0\"] i1[\"1\"] i2[\"2\"] i3[\"3\"]
i4[\"4\"] i5[\"5\"] i6[\"6\"] i7[\"7\"]
s8[\"\'d\'\"] s9[\"\"岛\"\"] s10[\"null\"] s11[\"null\"]
s12[\"null\"] s13[\"null\"] s14[\"\'g\'\"] s15[\"\"高\"\"]
i8[\"8\"] i9[\"9\"] i10[\"10\"] i11[\"11\"]
i12[\"12\"] i13[\"13\"] i14[\"14\"] i15[\"15\"]
end
style s0 fill:#00FF00;
style s1 fill:#00FF00;
style s2 fill:#00FF00;
style s3 fill:#00FF00;
style s4 fill:#808080;
style s5 fill:#808080;
style s6 fill:#808080;
style s7 fill:#808080;
style s8 fill:#00FF00;
style s9 fill:#00FF00;
style s10 fill:#808080;
style s11 fill:#808080;
style s12 fill:#808080;
style s13 fill:#808080;
style s14 fill:#00FF00;
style s15 fill:#00FF00;
style i0 fill:#0f0,stroke:#fff;
style i1 fill:#0f0,stroke:#fff;
style i2 fill:#0f0,stroke:#fff;
style i3 fill:#0f0,stroke:#fff;
style i4 fill:#fff,stroke:#fff;
style i5 fill:#fff,stroke:#fff;
style i6 fill:#fff,stroke:#fff;
style i7 fill:#fff,stroke:#fff;
style i8 fill:#0f0,stroke:#fff;
style i9 fill:#0f0,stroke:#fff;
style i10 fill:#fff,stroke:#fff;
style i11 fill:#fff,stroke:#fff;
style i12 fill:#fff,stroke:#fff;
style i13 fill:#fff,stroke:#fff;
style i14 fill:#0f0,stroke:#fff;
style i15 fill:#0f0,stroke:#fff;
参考资料
-
Block Diagrams Documentation
-
[Java] 浅析 Set.of(…) 方法
-
OpenJDK 中的
- Map.java
- ImmutableCollections.java



还没有评论呢,快来抢沙发~