Java双大括号初始化:是优雅的语法糖,还是隐藏的陷阱?​​

HYF Lv4

最近在 Code Review 项目代码的时候,发现有同事很喜欢使用 语法糖 {{ }} 来初始化代码,翻阅了几个项目都是大量的这种写法。到底是高效简洁的利器糖还是糖衣炮弹呢?我们往下分析。

解构双大括号初始化:原理探秘​

场景案例:

以下是项目中找到的部分案例,在下个章节我们来分析一下各个案例的问题。

案例1: (👉优化方案)
示例代码1
案例2: (👉优化方案)
示例代码2
案例3: (👉优化方案)
示例代码3
案例4: (👉优化方案)
示例代码4
案例5: (👉优化方案)(没找到 Map 相关的初始化,我自己补一个,hhhhh)
示例代码5

原理解析

接下来我们用我的案例5来解释一下这个语法糖的原理:

外层大括号 {}​: 它不是在定义一个代码块,而是在定义一个匿名内部类。new HashMap<>() {...}创建了一个继承自 HashMap 的匿名子类

内层大括号 {}​: 这是匿名内部类的实例初始化块。实例初始化块会在构造函数之前被执行。

合二为一的效果​: 代码实际上是创建了一个匿名子类,并在其初始化块中调用了 put方法。这等价于:

1
2
3
4
5
6
7
8
9
10
Map<String, String> map = new AnonymousHashMapClass();

class AnonymousHashMapClass extends HashMap<String, String> {
// 实例初始化块
{
put("a", 1);
put("b", "2");
put("c", '3');
}
}

优雅

雅!是在太雅了!!但是,古尔丹,代价是什么呢?

优雅背后的代价:劣势深度剖析​

这是文章的重点,我将详细解释为什么我不推荐使用该方式。

常见风险问题

性能开销

​类加载开销​: 每次使用都会创建一个新的匿名类。JVM需要加载、验证、准备和解析这个新类。

​内存占用​: 每个匿名类都会在永久代(Java 8之前)或元空间(Java 8+)中占用内存。大量使用会导致元空间压力增大。

总结就是编译器会因此多生成一个 *.class 文件,运行时 JVM 也得加载这个额外类,给 JVM 带去额外负担,还耗费更多内存

内存泄漏风险

隐含的this引用​: 匿名内部类会隐式持有其外部类的引用。

场景举例​: 如果这个Map被长期存活的对象(如一个静态集合)引用,那么由于匿名内部类持有外部类的引用,会导致外部类实例无法被垃圾回收,即使它早已不再使用。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ArticleProcessor {
private final String content;

public ArticleProcessor(String content) {
this.content = content;
}

public int countTerm(String term) {
return ...; // 统计术语出现次数(实现省略)
}

public Map<String, Integer> getTermFrequency() {
return new HashMap<String, Integer>() {{
put("AI", countTerm("AI"));
put("ML", countTerm("Machine Learning"));
put("NLP", countTerm("Natural Language Processing"));
}};
}
}

使用双大括号 {{}} 初始化匿名内部类时,生成的 HashMap 子类会隐式持有外部类 ArticleProcessor 的引用,导致在实例初始化块中调用 countTerm() 方法时必须通过该引用访问外部类实例;这意味着当程序长期持有 getTermFrequency() 返回的 Map 对象时(如放入全局缓存),即使 ArticleProcessor 实例本身已不再被直接引用,也会因匿名内部类的隐式引用强制维持其存活状态,从而阻止垃圾回收机制释放该实例及其关联的大对象(如 content 文本内容),最终引发内存泄漏。

序列化问题​

匿名内部类的序列化行为可能与预期不符,因为其名称是编译器生成的(如 OuterClass$1),反序列化时容易出现问题。

还是以上文案例举例说明:当尝试序列化 getTermFrequency() 返回的 Map 对象时,会触发 java.io.NotSerializableException 异常。这是因为匿名内部类隐式持有的 ArticleProcessor 外部引用本身未实现序列化接口。若试图通过让 ArticleProcessor 实现 Serializable 来规避此问题,反而会引发更严重的后果:序列化过程将强制包含整个 ArticleProcessor 实例(包括可能包含大量文本的 content 字段),而客户端在反序列化时若缺少 ArticleProcessor 类定义,则会抛出 ClassNotFoundException,导致系统健壮性降低。

代码可读性与工具支持​

对于不熟悉该语法的开发者来说,会增加理解成本。

经查阅资料,一些代码分析工具(如 FindBugs, SonarQube)会将其标记为代码坏味或潜在问题。

equalsgetClass() 方法的行为可能变得奇怪,因为匿名子类与普通HashMap不属于同一个类。

but,客观评价其优点,该方法在语法上的简洁性​,确实极大地减少了模板代码,尤其是在编写测试用例或临时示例时;且所有初始化操作都在定义的地方完成,上下文清晰。

一些安全使用场景

一些​单次或者低频使用的工具方法

1
2
3
public List<String> getTempList() {
return new ArrayList<>() {{ add("A"); add("B"); }};
}

要求:方法局部变量且不会被传递到外部

​静态常量初始化

1
2
3
4
private static final Map<String, String> CONSTANT_MAP = 
Collections.unmodifiableMap(new HashMap<>() {{
put("K1", "V1");
}});

建议使用 Collections.unmodifiableMap 包装避免修改

示例代码优化

案例1、案例2优化

我们仔细看一下代码:(原文缩进都不给一下,看真难受)

案例1中的关键代码:

1
new ArrayList<String>() {{ this.add(applyDTO.getReference()); }}

**案例2中的关键代码:

1
list = new ArrayList<UserDTO>() {{ this.add(userService.get(applyDTO.getApplyUser().getId())); }};

本质上都是对 ArrayList 进行初始化操作,那么,我们可以使用:

1
2
3
4
5
6
7
// 案例1:
// 如果存在多元素,在 asList 内以逗号隔开依次补充即可
// Arrays.asList会基于 Java 泛型机制和编译器类型进行类型推断,下同
Arrays.asList(applyDTO.getReference())

// 案例2:
list = Arrays.asList(userService.get(applyUser().getId()));
1
2
3
4
5
6
// 案例1:
// 该方案仅适用于单元素场景(表骂了表骂了,有局限但符合原始场景案例,hhhhhh)
list = Collections.singletonList(userService.get(applyDTO.getApplyUser().getId()));

// 案例2:
list = Collections.singletonList(userService.get(applyDTO.getApplyUser().getId()));

方案一采用基于数组的包装实现,相比 ArrayList 更加轻量,同时避免了原方案中匿名类的生成,降低了元空间的开销。但该方案也存在一些局限:可能存在“半可变”的陷阱,例如:

1
2
3
4
5
// 抛出UnsupportedOperationException
list.add(new UserDTO());

// 抛出UnsupportedOperationException
list.remove(0);

且是有数组引用泄露的风险——由于其底层直接使用原始数组,外部可能意外修改数组内容。

方案二几乎实现了零内存开销:它返回的是不可变的单例集合(得益于 JVM 级别的优化),无需创建新对象,而是复用已有的单例实例。该方案具有绝对的安全性,既没有匿名类相关的问题,也不会带来内存泄漏风险,同时还天然具备线程安全性。此外,其语义更加明确,能够清晰表达“单元素集合”的设计意图。不过,该方案也有一定的局限性:集合完全不可变,无法增删或修改,且只能包含一个元素。

案例3、案例4优化

我们仔细看一下代码:(原文依旧是令人窒息的缩进问题)

案例3中的关键代码:

1
2
3
4
new SendCountDTO() {{
this.setLoginName(finalLoginName);
this.setEmail(email);
}};

案例4中的关键代码:

1
2
3
4
5
6
7
newRecords.forEach(item -> {
item.setExperiment(new WxiExperimentApplyDTO() {{ this.setId(""); }});
item.setUser(new UserDTO() {{ this.setId(""); }});
item.setEthic(new WxiEthicApplyDTO() {{ this.setId(""); }});
item.setApproveDate(null);
item.setStatus(WxiConstants.ANIMAL_STATUS_IDLE);
});

本质上都是对对象进行一个初始化操作。
案例3这种在 Service 层中的写法,会隐式持有外部类( xxxService )的引用。但由于 xxxServiceSpring 单例(生命周期=应用运行期),且 result 是方法局部变量(方法结束后可被 GC 回收),实际泄漏风险较低。无直接安全漏洞,但匿名类会增加元空间负担,且可能存在序列化风险 :匿名类可能引发序列化异常(如 Jackson@JsonIgnoreProperties 忽略多余字段)。而案例4在上面的问题上,额外还有一个性能开销,每次循环都创建新类(首次加载时),可能引发元空间 OOM

不禁想问问,为了炫技而匿名使用内部类?把语法糖当饭吃呢?

那么依旧给出几个优化方案:

在参数量少的情况下,我们可以使用构造器来处理赋值问题

SendCountDTO 类中添加构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   // 案例3:
// 注:如果未添加无参构造器,需手动添加或使用 @NoArgsConstructor 注解
public SendCountDTO(String loginName, String email) {
super();
this.loginName = loginName;
this.email = email;
}

// 案例4:
public WxiExperimentApplyDTO (String id) {
this.id = id;
}

public UsrDTO (String id) {
this.id = id
}

public WxiEthicApplyDTO (String id) {
this.id = id
}

然后更改原代码为:

1
2
3
4
5
6
7
8
9
10
11
// 案例3
new SendCountDTO(finalLoginName, email);

// 案例4
newRecords.forEach(item -> {
item.setExperiment(new WxiExperimentApplyDTO(""));
item.setUser(new UserDTO(""));
item.setEthic(new WxiEthicApplyDTO(""));
item.setApproveDate(null);
item.setStatus(WxiConstants.ANIMAL_STATUS_IDLE);
});

在参数量多的情况下,我们可以使用之前讲过的「构建者模式」Java 使用构建者模式创建对象实例来创建对象

前置条件(仅用案例4举例,不然废话太多了…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 为显式继承的父子类关系时,在父类及子类都添加 @SuperBuilder 注解,如:
// 父类
@SuperBuilder
public abstract class BaseDTO<T> implements Serializable {

private String id;

private LocalDateTime createTime;

// other field
}

// 子类
@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
public class SendCountDTO extends BaseDTO {

private String loginName;

private String email;

// other field
}

// 没有显式继承,为根类/顶级类时,在根类添加 @Builder 注解,如
@Data
@Builder
@EqualsAndHashCode(callSuper = true)
public class SendCountDTO implements Serializable {

private String id;

private String loginName;

private String email;

// other field
}

然后更改原代码为:

1
2
3
4
SendCountDTO sendCountDTO = SendCountDTO.builder()
.loginName(finalLoginName)
.email(email)
.build();

在案例4中,还存在一个设计层面上的问题。从代码逻辑上看,应该是在某些条件下,需要将这些符合条件元素的一些字段做一个初始化处理。批量处理1000条数据的话,将会产生3000个空元素及匿名类!所以其实可以在循环外添加3个空对象,循环内重复复制即可,例如:

1
2
3
4
5
6
7
8
9
10
11
final WxiExperimentApplyDTO experimentApplyDTO = new WxiExperimentApplyDTO("");
final UserDTO userDTO = new UserDTO("");
final WxiEthicApplyDTO ethicApplyDTO = new WxiEthicApplyDTO("");

newRecords.forEach(item -> {
item.setExperiment(experimentApplyDTO);
item.setUser(userDTO);
item.setEthic(ethicApplyDTO);
item.setApproveDate(null);
item.setStatus(WxiConstants.ANIMAL_STATUS_IDLE);
});

由此引发思考,能否有更好的设计方案呢?于是有了下文:

方案对比表

维度匿名内部类双括号初始化带参构造器方案@SuperBuilder
性能❌ 最差 (类加载 + 实例化开销)✅ 最优 (原生对象创建)⚡️ 接近最优 (编译期生成)
内存占用❌ 元空间膨胀 + 隐式外部引用✅ 无额外开销✅ 无额外开销
代码简洁度⚠️ 中等 (但语法特殊)⚠️ 中等 (需维护构造器)✅ 最优 (声明式)
可读性❌ 差 (嵌套语法)✅ 好✅ 极好 (流畅 API)
线程安全❌ 不安全✅ 安全✅ 安全
序列化兼容性❌ 可能异常✅ 正常✅ 正常
调试友好度❌ 差 (含 $1 匿名类)✅ 好✅ 好

更优的架构设计思考

空对象模式

在系统中统一定义 EmptyObjects 工具类

1
2
3
4
5
6
7
8
9
10
public class EmptyObjects {

public static final UserDTO USER = UserDTO.builder().id("").build();

public static final WxiExperimentApplyDTO EXPERIMENT = WxiExperimentApplyDTO.builder().id("").build();

public static final WxiEthicApplyDTO ETHIC = WxiEthicApplyDTO.builder().id("").build();

// other
}

领域模型优化

records 所属的类中增加 clearAssociations() 方法

1
2
3
4
5
6
7
public void clearAssociations() {
this.user = EmptyObjects.USER;
this.experiment = EmptyObjects.EXPERIMENT;
this.ethic = EmptyObjects.ETHIC;
this.approveDate = null;
this.status = WxiConstants.ANIMAL_STATUS_IDLE;
}

最后将原代码修改为:

1
newRecords.forEach(WxiAnimalMsgDTO::clearAssociations);

案例5优化

Map 的优化方案简单粗暴:

如果现在使用的是 Java9+ ,这是当前的首推方案​​(如果需求是不可变集合):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 小于10对,使用 Map.of()
Map<String, String> scores = Map.of(
"key1", "value1",
"key2", "value2",
"key3", "value3"
);

// 超过10对,使用 Map.ofEntries()
Map<String, String> map = Map.ofEntries(
entry("key1", "value1"),
entry("key2", "value2"),
entry("key3", "value3"),
// ......
entry("key11", "value11")
);

如果你正在使用 Java8,那你也可以使用第三方库例如(Guava):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 小于5对,使用 ImmutableMap.of()
ImmutableMap<String, String> multiMap = ImmutableMap.of(
"key1", "value1",
"key2", "value2",
"key3", "value3"
);

// 超过5对,使用 ImmutableMap.builder()
ImmutableMap<String, String> map = ImmutableMap.<String, Integer>builder()
.put("key1", "value1")
.put("key2", "value2")
.put("key3", "value3")
// ......
.put("key6", "value6")
.build();

我们也可以自行实现 Builder 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author yofeng
* @date 2025/8/01
* @Description
*/
public final class MapBuilder<K, V> {
private final Map<K, V> map = new HashMap<>();

public static <K, V> MapBuilder<K, V> builder() {
return new MapBuilder<>();
}

public MapBuilder<K, V> put(K key, V value) {
map.put(key, value);
return this;
}

public MapBuilder<K, V> putAll(Map<? extends K, ? extends V> sourceMap) {
map.putAll(sourceMap);
return this;
}

public Map<K, V> build() {
return new HashMap<>(map);
}
}

源代码改为:

1
2
3
4
Map<String, String> map = new MapBuilder<String, String>()
.put("key1", "value1")
.put("key2", "value2")
.build();

方案三其实也是一种构建者模式的体现

方案对比表

特性双括号初始化方案一(Java9+ Map.of)方案二(Guava ImmutableMap)方案三(自定义Builder)
语法简洁度★★★★☆ (最简洁)★★★★★ (极简)★★★★☆★★★☆☆
线程安全❌ (HashMap非线程安全)✅ (返回不可变集合)✅ (返回不可变集合)❌ (返回普通HashMap)
空值支持❌ (禁止null键值)❌ (禁止null键值)
内存开销❌ (匿名类+实例初始化块)✅ (最优)✅ (较优)✅ (常规对象)
扩展性❌ (初始化后不可修改)❌ (不可变集合)❌ (不可变集合)✅ (链式调用灵活扩展)
第三方依赖需要Guava
适用场景快速原型/临时使用Java9+小规模不可变集合需要不可变集合且可接受第三方依赖需要灵活构建可变Map

总结

总而言之,双大括号初始化 {{}} 是一个典型的“为了一时便利而牺牲长远健康”的案例。它通过创建匿名子类和实例初始化块的方式,用语法糖的外衣包裹住了性能开销、内存泄漏风险和序列化问题的内核.

因此,我们的建议非常明确:​在生产代码中,请将双大括号初始化视为一种应避免使用的“奇技淫巧”,转而拥抱官方提供的现代、安全的特性。让我们做更明智的选择,为代码的长期健康和可维护性负责。

  • 标题: Java双大括号初始化:是优雅的语法糖,还是隐藏的陷阱?​​
  • 作者: HYF
  • 创建于 : 2025-08-01 20:41:39
  • 更新于 : 2025-08-01 20:41:39
  • 链接: https://yofeng.love/double-brace-initialization-5a86b6e93b82/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。