Code review 时那行 Arrays.asList
12 月底做代码评审,看到一个新同事写的状态判断:
private static final List<String> PAID_STATUS = Collections.unmodifiableList(
Arrays.asList("PAID", "SHIPPED", "FINISHED"));
能想到用 unmodifiableList 包一层,说明他有"别让别人改坏我这个常量"的意识,这点是好的。我问他:为什么不用 List.of?他想了一下说:「不都是不可变吗?」
不一样,而且差别不小。我们项目是 JDK 11(部分新服务已经上了 17)。
先看最直观的写法差异
// Java 8 及以前,三种常见写法
List<String> a = new ArrayList<>();
a.add("PAID");
a.add("SHIPPED");
a.add("FINISHED");
a = Collections.unmodifiableList(a);
List<String> b = Collections.unmodifiableList(Arrays.asList("PAID", "SHIPPED", "FINISHED"));
// Java 9 之后
List<String> c = List.of("PAID", "SHIPPED", "FINISHED");
一行变三行还是小事,关键是语义。
unmodifiableList 是"视图",不是"副本"
这是最容易踩的坑。我写了一段代码给那位同事看:
List<String> origin = new ArrayList<>();
origin.add("A");
List<String> unmodifiable = Collections.unmodifiableList(origin);
System.out.println(unmodifiable); // [A]
origin.add("B"); // 改的是原始 list
System.out.println(unmodifiable); // [A, B] ← 视图跟着变了
unmodifiable.add("C"); // Exception in thread "main"
// java.lang.UnsupportedOperationException
Collections.unmodifiableList 返回的 UnmodifiableList 内部只是持有原 list 的引用,所有读方法转发过去,写方法抛 UnsupportedOperationException。它保护的是"通过我这个引用去改",保护不了"原始引用去改"。
在上面的 Arrays.asList 例子里,因为那个 Arrays$ArrayList 是临时的、没人持有,实际效果是安全的。但如果你像我第一段代码那样把 origin 留了个字段,就出问题了。
List.of 是完全独立的实例:构造时把元素拷进内部数组,之后跟任何外部引用都没关系。
List.of 拒绝 null
这个限制刚上手时很容易撞上:
List.of("A", null, "C");
// Exception in thread "main" java.lang.NullPointerException
// at java.base/java.util.ImmutableCollections$ListN.<init>(ImmutableCollections.java:237)
Set.of("A", null);
// java.lang.NullPointerException
Map.of("k1", "v1", "k2", null);
// java.lang.NullPointerException
而 Arrays.asList 是允许 null 的。我当时把我们配置中心读出来的默认值往 List.of 里塞,配置没配全时读到 null,启动时直接 NPE,浪费了十几分钟。
如果你的数据源可能有 null,得先过滤:
List<String> safe = Arrays.stream(rawArray)
.filter(Objects::nonNull)
.collect(Collectors.collectingAndThen(Collectors.toList(), List::copyOf));
List.copyOf 是 Java 10 加的,作用是把任意集合转成不可变副本,同样拒绝 null。
Set.of 会拒绝重复元素
Set.of("A", "A");
// java.lang.IllegalArgumentException: duplicate element: A
而 new HashSet<>(Arrays.asList("A", "A")) 会静默去重。我挺喜欢这个设计的:写死在代码里的重复元素基本都是手误,早失败比晚失败好。但如果你是从外部数据构造,得先 distinct():
Set<String> s = items.stream().distinct().collect(
Collectors.collectingAndThen(Collectors.toSet(), Set::copyOf));
Map.of 最多十对,超了用 ofEntries
Map.of 的重载只写到十个键值对,参数是 (k1,v1,k2,v2,...) 这种平铺形式。超过十对只能换 API:
Map<String, Integer> m = Map.ofEntries(
Map.entry("PAID", 1),
Map.entry("SHIPPED", 2),
Map.entry("FINISHED", 3),
Map.entry("CLOSED", 4),
// ... 想写多少写多少
);
注意 Map.entry 返回的也是不可变 Map.Entry,它同样拒绝 null,而且不可 Serializable,别往需要序列化的地方塞。
一个需要注意的行为:迭代顺序是随机的
这是 Java 9 故意加的防护。ImmutableCollections 里有个 SALT32L,在 JVM 启动时用 System.nanoTime() 初始化,Set.of 和 Map.of 的底层数组会按这个盐值做一次旋转:
// ImmutableCollections 源码片段
static final int SALT32L;
static final boolean REVERSE;
static {
// to generate a reasonably random and well-mixed SALT, use an arbitrary
// value (a slice of pi), multiply with a random seed, then pick
// bits 32..63 of the 128-bit result.
long color = 0x243F_6A88_85A3_08D3L; // slice of pi
long seed = CDS.getRandomSeedForDumping();
if (seed == 0) { seed = System.nanoTime(); }
SALT32L = (int)((Math.multiplyHigh(color, seed) >> 16) & 0xFFFF_FFFFL);
REVERSE = (SALT32L & 1) == 0;
}
所以同一段代码,今天跑和明天跑,Set.of("A","B","C") 的遍历顺序可能不一样:
Set<String> s = Set.of("A", "B", "C", "D");
System.out.println(s); // 这次是 [D, A, C, B]
// 下次可能是 [A, C, B, D]
官方的用意是:防止有人不小心依赖了不可变集合的迭代顺序,把这种隐式依赖在测试阶段就暴露出来,而不是上线后在某个版本升级时炸掉。List.of 不受影响,它按参数顺序迭代。
我们的测试里就炸过一次。有个断言写的是:
assertEquals("[SHIPPED, PAID, CLOSED]", orderService.queryStatus().toString());
改成 Set.of 之后这个断言开始随机失败。正确写法是比较集合本身,而不是比较它的字符串表示:
assertEquals(Set.of("SHIPPED", "PAID", "CLOSED"), orderService.queryStatus());
顺手记一下性能
我做了个小测试,构造一个三元素 list 并遍历一千万次:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ListCreateBench {
@Benchmark public List<String> arraysAsList() {
return Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
}
@Benchmark public List<String> listOf() {
return List.of("A", "B", "C");
}
}
Benchmark Mode Cnt Score Error Units
ListCreateBench.arraysAsList avgt 9 15.823 ± 0.412 ns/op
ListCreateBench.listOf avgt 9 4.107 ± 0.093 ns/op
List.of 快约 3.8 倍,原因是 ListN 直接把可变参数数组当作内部存储(没有额外拷贝),而 unmodifiableList(Arrays.asList(...)) 要 new 两个对象。单次的 11 ns 差距不算什么,但如果在一个高频方法里每次都构造,累积起来就可观了。
就写到这。如果哪天你也被《JDK 9+ 的新集合工厂方法与不可变集合》里同一个坑绊住,回来翻这篇,能省半小时。