---
title: "Java 配置已经只读却还在变：区分 Map 视图与快照"
date: 2026-11-13
slug: java-map-copy-vs-view
canonical: https://kernelranch.com/blog/2026/11/java-map-copy-vs-view
markdown_url: https://kernelranch.com/blog/2026/11/java-map-copy-vs-view.md
tags: ["java", "collections", "configuration"]
---配置中心返回了一个无法 put 的 Map，调用方却发现值在热更新后变了。Collections.unmodifiableMap 是底层 Map 的只读视图，后续修改仍会透过视图。Map.copyOf 可以固定映射关系，内部的可变对象还需要单独复制。

## 1. 三次修改验证三层边界

保存为 Lab.java，运行 java -ea Lab.java。

```java
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class Lab {
    public static void main(String[] args) {
        var routes = new ArrayList<>(List.of("/health"));
        var source = new HashMap<String, List<String>>();
        source.put("public", routes);
        var view = Collections.unmodifiableMap(source);
        var shallow = Map.copyOf(source);

        var detached = new HashMap<String, List<String>>();
        source.forEach((key, value) -> detached.put(key, List.copyOf(value)));
        var snapshot = Map.copyOf(detached);

        source.put("admin", List.of("/admin"));
        assert view.containsKey("admin");
        assert !shallow.containsKey("admin");
        System.out.println("new map entry: view=true, copy=false");

        routes.add("/metrics");
        assert shallow.get("public").size() == 2;
        assert snapshot.get("public").equals(List.of("/health"));
        System.out.println("nested list sizes: shallow=2, snapshot=1");

        try {
            view.put("other", List.of());
            throw new AssertionError("expected read-only view");
        } catch (UnsupportedOperationException expected) {
            System.out.println("direct mutation: rejected by view");
        }
    }
}
```

本机实测输出如下。

```text
new map entry: view=true, copy=false
nested list sizes: shallow=2, snapshot=1
direct mutation: rejected by view
```

## 2. 按配置模型冻结每一层

| 方法 | 固定了什么 |
|---|---|
| unmodifiableMap | 阻止通过这个引用修改映射 |
| Map.copyOf | 固定键和值的引用关系 |
| Map.copyOf 配合 List.copyOf | 固定映射和列表的成员关系 |

示例的叶子是不可变 String，因此复制 Map 和 List 就足够。若叶子是可变 DTO，还需要转换为不可变值。copyOf 拒绝 null，接入旧配置前要决定缺失、空值和空列表的含义。

热更新时，构造并校验完整快照，再通过 volatile 字段或 AtomicReference 发布新引用。读取者每次业务操作只取一次引用，避免在同一次操作里交叉读取两个版本。制作副本时也要确保源数据稳定，例如从刚解析、尚未共享的本地对象构造。

## 小结

发布配置前同时验证映射和嵌套值的生命周期。用源对象后续修改来检验快照是否真正独立。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| Java 17 Map.copyOf | https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Map.html#copyOf(java.util.Map) | 快照与不可修改映射约束 |
| Java 17 Collections | https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html#unmodifiableMap(java.util.Map) | 只读视图对底层映射的读取 |
| Java 17 List | https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/List.html#copyOf(java.util.Collection) | 列表副本与空值限制 |

## 核验说明

- OpenJDK 17.0.19 实测映射、嵌套列表和修改视图的三种行为。并发发布方式是接入建议。
