永远不要使用双花括号初始化实例,除非你想OOM!
生活中的为难无处不在,有时候你仅仅想简略的装一把,但某些“老同志”总是在不经意之间,给你无情的一脚,踹得你几乎无法呼吸。
但谁让咱年轻呢?吃亏要趁早,前路会更好。
喝了这G p ^ $口温热的鸡汤,咱们来聊聊是怎么回事。
事情是这样的,在一个不大不小的项目中,小王写下了这段代码:
Map<String, String> map = new HashMap() {{
put("map1",l C m "value1");
put("map2", "value2");
put("map3", "value3");
}};
map.forEach((k, v) -> {
Systeb _ mm.out.println("key:i h D" + k + " value:" + v);
});
原本是用它来代替U 8 w l H下面这段代码的:
Map<String, String> map = new HashMap();
map.put("map1", "value1");
map.pg 9 ` a ^ I @ut("map2", "value2");
map.put("map3", "value3");
map.forEach((k, v) -> {
System.out.p/ p Krintln("keyB 9 $:" + k + " value:" + v);
});
两块代码的履行成果也是完全相同的:
key:map3 value:value3
key:map2 value:value2
key:map1 value:value1
所以小王正在满意的把这段代码介绍给部分新来的妹子小甜甜看,却不巧被正在经过的老张也看到了。
老张原本仅仅想给昨日的枸杞X [ ^ j N u P再续上一杯 85 的热水,但说来也巧,刚好撞到了一次能在小甜甜面前秀技能的一波机会d + i ` V ( s u t,所以习惯性的整理 1 V B q #了一下自己稀少的秀发,便敞开了 diA % @ss 模式。

“小王啊,你这个代码问题很大啊!”
“怎么能用双花括号初始化实例呢?”
此时的小王被问的一脸懵逼,心里有无数个草泥马奔腾而过,心想你这头老牛竟然也和我争这颗嫩草,但心里却有一种不祥的预感,感觉自己要输,瞬间羞涩的不知该说啥,只能红着小脸,悄悄的“嗯?”了一声。
老张:“运用双花括号初始化实例是会导致内存溢出的啦!侬不晓R = M ; p ) B 9得嘛?”
小王沉默了片刻,仅仅凭借着以往的经历g ^ h来看,这“老家伙”仍是有点东西的,所以唐塞! q J !的“哦~”了一声,似乎自己理解了怎么回事相同,? B ` b W 2 v,其实心里仍然迷茫的一匹,为了不让其他同事发现,只得这般作态。
所以片刻的唐塞,待老张离去之后,才悄悄的打开了 Google,默默的搜索了! j 7 J % &一下。
小王:哦,原来~ d $ 4 c . A A 3如此……
双花括号初始化剖析
首要,咱们来看运用` % c u双花括号初始化( f c的本质是什么?
以咱们这段代A ^ & h l F _ j v码为例:
Map<String, String> map = newl j ~ , G HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3"i u o m | ) 0 z, "value3");
}};
这段代码其实V Y 0 是创建了匿名内部类,然后再进行初始化代码块。
这一点咱们能够运用命令 javac
将代码编译成字节码之后发! T a i 3现,咱们发现之前的一个类被编译成两个字节码(& c 2 f.ch ` y + 0 6 lass)文件,如下图所示:

咱们运用 Idea 打开 DoubleBracket$9 4 q p x C1.classE c ) . q % : ~
文件发现:
import java.util.HashMap;
class Dou! 9 ( x h X AbleBracket$1 extends HashMap {F W 0 P h k x
DoubleBracker & H [ 0 N t$1(DoubleBracket var1) {
this.this$0 = var1;
this.put("map1", "value1");
this.put("map * w2", "value2");
}
}
此时@ c 4 X F咱们能够承认,它就是一个匿名内部类。那么问题来了,匿名内部类为什么会导致& c Z O k _ 内存溢出呢?
匿名内部类的“锅”
在 Java 言语中非静态内部类会持有外部类的引证,然后导致 GC 无法回收这部分代码的引证,以至于形成内6 = X 8存溢出。
思考 1:为什么要持有外部类?
这个就要从匿名内部类的J w g ^ I o $ W规划说起了,在 Java 言语中,非静态匿名内部类的主要效果有两个。
1、当匿名内部L K r类只在外部类(主类)中运用时,匿名内部类能够让u 9 外部不知道它的存在,然后减少了代码的维护工作。
2、当匿名内部类持有外部类时,它就能够直接运用外部类中的变量了,这样能够很便利的完结调用,p * x C ) . W 4如下代码所示:
publ) & i K x U (ic claF Q P K Kss DoubleBracket {e Q d c p e a
private static String userName = "磊哥"( b e ^ ^ /;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Map<String, String> map = new HashMap() {{
put("map1", "value1");
put("map2", "value2");
put("map3", "value3");
put(userName, userName);
}};
}
}
从上述代码能t } ` A = D u c [够看出在 HashMap
的办法内部,能够直接运用外部类的变量 userName
。
思考 2:它是怎么持q # i 6 e h有外部类的?
关于匿名内部类是怎么耐久外部目标的,咱们能够经过检查匿名内部类的字节码得知,咱们运用 javap -c DoubleBracket$1.clasE [ o V +s
命令进A u o f g _ 7 c行检查,其间 $1
为以匿名类的字7 C H U ! K节码,字节码的内容如下;
javap -c DoubleBracket$1.class
Compiled fre 7 ^ ; @ A Q Y uom x o 0 ? + Y"DoubleBracket.java"
class com.example.DoubleBracket$1 extends java.util.HashMap {
final com.example.DoubleBracket this$0;
com.example.DoubO s ~ A ( H _ ; ZleBracket$1(com.example.Doub9 $ p _leBracket);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/example/DoubleBracket;
5: aload_0
6: invokespecial #7 // Method java/util/HashMap."<init>":()V
9: aload_0
10: ldc #13 // String map1
12: ldc #15 // String value1
14: invokevirtual #17 // Method put:(LjX 1 ` I X x X 6ava/lang/Object;Ljv 2 Kava/lang/O{ 6 %bject;)Ljava/lang/Object;
17: pop
18:9 V n N Q _ ! l ] aload_0
19: ldc #21 // String map2
21: ldc #23 // String value2
23: invokevirtual #17 // Method put:(Ljava/lJ J 8 $ 3 b @ | Iang/Object;Ljava/lang/Object;)Ljava/lang/Object;
26: pop
27: return
}
其间,要害代码的在 putfield
这一行,此行表明有一个对 DoubleBracket
的引证被存入到 this$0
中,也就是说这个匿名内部类持有了外部类的引证。
如果您] u h觉得以上字节码不够直观,不要紧,咱们用下面的实践的代码来证明一下:
import javaU I ) {.lang.reflect.h A d / N e G m YFip $ s [ 9 B (eld;
import: s Q a m % java.util.HashMap;
import java.util.Map;
public class DoubleBr@ P x S Y #acket {
public st] ) M & R Q latic void main(String[] args) throws NoSuchFieldException, Illegall 2 _ 3 6 a ] 6AccessException {
Map map = new DoubleBracket().createMap();
// 获取一个类的所有字段
Field field = map.getClass().getDeclaredField("this$0");
// 设置答应m | w | O z *办法私有的 pS u 7rivate 润饰的变量
field.setAccessO m O ;ible(true);
System.out.println(field.get(map).getClass());
}
public Map createMap() {
// 双w Z Z u花括号初始化
Map map = new HashMap() {{
put("map1", "valD : t q due1~ z X j X");x l s b K % h
put("map2", "value2");
put("map3", "valueW u o3");
}};
return map;
}
}
当咱们敞开调试模式时,能够看出 map
中持有了外部目标 DoubleBracket
,如下图所示:

以上代码的Q R 1履行成果为:
claR , 9 ss com.example.DoubleB4 | 0 c ~ +racket
从以上程序输出成果能够看出:匿名g I 1 V ! ~ h + 3内部类持有了外部类的引证,因此咱们才能够运用 $0
正常获取到外部类,并输出相关的类信息。
什么情况会导致内存走漏?
当咱们把以下正常的代码(无返回值):
public void createMap() {
Map map = new HashMap() {{
put("map1@ 2 T z u", "value1");
put("map2", "value2");
put("map3", "value3");
}};
// 业务处理....
}
改为下面这个样子时(返回了 M+ ` a n { p Qap 调集),或许会形成内存走漏:
public Map createMap() {
Map map = new HashMa: ( & T I Jp() {{
put("ms A h Nap1", "value1");
put("map2", "value2");
put("map3",R L + X s L [ 4 G s 6 ! p"value3");
}};
return map;
}
为什么用了「X B 6 F x P : E或许」而不是「必定」会形成内存走漏?

这是由于当此 map
被赋值为其他类特点时,或许会导致 GC 搜集时不清理此目标,这时候~ k W – Z p才会导致内存走漏。能够重视我「Java中文社群」后面会专门写一篇关于此问题的文章。
怎么确保内存不走漏?
要想确保双花扣号不走漏,办法也很简略,只需求将R w n . – & } map
目标声明为 static
静态类型的就能够了,代码如下:
public static Map createMap() {
Map mak R H vp = new HashMap() {{
put("map1; & $ g 5 D 0 R ?", "value1");
put("map2", "value2");
put("map3", "value3");
}};
return map;
}
什么?你不相信!

不要紧,咱们用事实说话,运用以上代码,咱们重新编译一份字节码,检查t U @ G ` p , l %匿名类的内容如下:
javap -c DoubleBracket$1.class
Compiled from "DoubleBracket.javo = aa"
class com.example.DoubleBracke$ + A h a } @ _t$1 extends java.util.HashMap {
com.example.DoubleBracket$1();
Code:
0: al@ $ moad_0
1: invokespecZ 3 - M Sial #1 // Method java/util/HashMap."<. r e p;init>":()V
4: a1 c t + U S l Kload_0
5: ldc #7 // String map1
7: ldc #9 // String value1
9: invo^ p akevirtual #11 // Method put:(Ljava/lang/Object;Ljava/l4 F :an% w D N P g/Object;)Ljava/lang/Object;
12:} x G r pop
13: aload_0
14: ldc #17 /~ $ ; V q // String map2
16: ldc #19 // String value2
18:C ~ y ; b T ~ invokevirtuY E !al #11 // Method put:(Ljava/lang/Object;LjavS z q Ia/lang/Object;)Ljava/lang/Object;
21: pop
22: aload_0
23: ldc #21 // String map3
25: ldc #23 // String value3
27: invokevirtual #11 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljav9 ( : Ga/lang/Object;
30: pop
31: return
}
从这次的代码咱们能够看出,现已没有 putfi3 ` e y 2 -eld
要害字x , @ L这一行了,也就是说# ] g * W a $静态r _ ; B N c 5匿名类不会持有外部目标的引证了。L P F $ 3 7 ( X s
为什么静态内部类不会持8 I * # D ( I有外部类的引证?
原因其实很简略,由于匿名内部类是静态的之后,它所引证的目标或特点也有必要是静态的了,因此就能够直接从 JVM 的 Method Area(办法区)获取到引证而无需耐久外部目标了。
双花括号的代替计划
即使声明为静态的变量能够防止内存走漏,但仍旧不建议这样运用,为什么呢?
原因很简l 4 ( . & l c B $略,项目一般都是需求团队协作的,假如那位` @ C :老兄在不知情的情况下把你的 stat@ I M ;ic
给删掉呢?这就相当于设置了一个隐形的“坑”,其他不知5 ] Z f b H e 4道的人,一不小心就跳进去了,所以咱们能够尝试一些其他的计划,比如 Java8 中的 Strm T H F x Beam API 和 Java9 中的调集工厂等。
代替计划 1:Stream
运用 Java8 中的 Stream API 代替,示例如下。原代码:
List<String> list = new ArrayLb D ~ Uist() {{
add(1 & b 1 $ 0"Java");
add("Redis");
}};
代代替码:
List<Strin] ^ ; H 0g> list = Stream.of("Java", "$ D k R G I ( ?Redis").collect(Coll) N P R 1 | Gectors.toList());
代替计划 2:调集工厂
运用调集Z M l工厂的 of
办法代替,示例如3 s 0 ( # ? o r下。原代码:
Map map = new HashMap() {{
put("map1", "value1");
put("map2* * K ^ E", "value2");
}};
代代替码:
Map map = Map.of("map1", "JavaH { e S B q r N !", "map2", "Redis");
明显运用 Java9 中E – V N ( 3的计划非常适合咱们,简略又酷炫,只可惜咱们还在用 Java 6…6…6… 心碎了一地。

总结
本文q H y { N 1 ~咱们讲了双花括号初始化由于会持有外部类的引证,然后能够会导致内存走漏的问题,还从字节码以及反射的层面演示了这个问题。
要想确保双花L T – ? N s S ` 7括号初始化不会出现内存走漏的办法也很简略,只需求被 static
润饰即可,但这样做仍是存在潜在的危险,或许会被某人不小心删除去,所以咱们+ h 9另寻它道,发现了能够运用 Java8 中的 Stream 或h j r Java9 中的调集工厂 of
办法代替“{{”。
最终的话
原创不易,点个「赞」再走呗p ^ : Q!
参考 & 道谢
www.ripjava.com3 ] G 4 o/article/129…
cloud.tencent.cE ~ L & E bom/developer/a…
hacpai.com/article/149…
重视大w 3 [ + ] b /众号「Java中文社群」回复“干货”,获取 50 篇原创干货 Top 榜。
