说在前面
一段时间以来,经常发现有人使用类似如下代码装逼耍酷
Map source = new HashMap(){{
put("firstName", "John");
put("lastName", "Smith");
put("organizations", new HashMap(){{
put("it", new HashMap(){{
put("id", "1234");
}});
put("oa", new HashMap(){{
put("id", "5678");
}});
}});
}};
我们称之为双花括号初始化(构造)实例,看起来是挺酷的!
如果你对这个语法只是拿来用过,知其然不知其所以然,没关系,实际上它挺简单的,一共做了两件事
- 使用下面的代码实例化一个继承自 Map 的匿名类
new HashMap(){}
- 然后使用下面的代码 继续实例化一个匿名类
{
put("id", "1234");
}
原理详见 Oracle 官方文档:Initializing Fields
实际上,这就是一个执行构造函数的过程~
为什么需要拒绝以这种方式初始化实例?
可读性 --这是最不重要的一个原因
双括号初始化实例的代码片段对于没有接触过的人来看,很难直观的理解他的作用,起码,从语法上来讲,就觉得别扭(虽然写的人挺爽);
每个实例一个类型
看起来,这段代码只是一个简单的实例化对象的过程,除此之外,什么事情都没有发生 ,实际上,他同时也会创建一个不可复用的(匿名内部)类!
如果你只是偶尔这么做一次,那还勉强可以接受,如果你在一个体量庞大的企业级应用中大范围的使用这种做法,那对 ClassLoader 来说简直就是灾难,他需要在堆内存中为这些生成的所有的匿名内部类保持一个引用!
如果你不信的话,你尝试建一个 Test 类,然后 copy 上面的代码,然后编译,你就会发现生成了如下文件:
Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class
holy shit!并不是你想象中理所当然的就一个 Test 类的 class...
最重要的原因是这样做会导致内存泄漏!!!
这是所有匿名内部类都有的一个致命的问题--他们会在内部维护一个外部容器类的引用,想像一下,你在一个庞大的应用中创建了一个如下代码类:
public class ReallyHeavyObject {
// Just to illustrate...
private int[] tonsOfValues;
private Resource[] tonsOfResources;
// This method almost does nothing
public void quickHarmlessMethod() {
Map source = new HashMap(){{
put("firstName", "John");
put("lastName", "Smith");
put("organizations", new HashMap(){{
put("0", new HashMap(){{
put("id", "1234");
}});
put("abc", new HashMap(){{
put("id", "5678");
}});
}});
}};
// Some more code here
}
}
成吨的资源需要在 ReallyHeavyObject 被 GC 的时候清理,然而你可能并没有意识到这一点,你只是:若无其事的调用了 quickHarmlessMethod(),完成了 ReallyHeavyObject 的使命,而且以后可能不会再调用第二次 ```
如果有一天,另一个开发者改动了你的代码,在 quickHarmlessMethod 方法中返回了 source 对象,如下代码所示:
public Map quickHarmlessMethod() {
Map source = new HashMap(){{
put("firstName", "John");
put("lastName", "Smith");
put("organizations", new HashMap(){{
put("0", new HashMap(){{
put("id", "1234");
}});
put("abc", new HashMap(){{
put("id", "5678");
}});
}});
}};
return source;
}
那你又犯了另一个罪行!如前文所说,所有匿名内部类都会在内部维护一个外部容器类的引用,所以,你在无心之下成功的将 ReallyHeavyObject 这个类通过此方法暴露了出去;
如果不信的话,请尝试以下代码:
public static void main(String[] args) throws Exception {
Map map = new ReallyHeavyObject().quickHarmlessMethod();
Field field = map.getClass().getDeclaredField("this$0");
field.setAccessible(true);
System.out.println(field.get(map).getClass());
}
会返回内容:
class ReallyHeavyObject
如果你还不信的话,就请打开调试模式自行查看!你会发现不仅这个匿名 Map 持有了外部类的引用,就连他的子类,也持有!
延伸
也许你会讲,第三个问题可以通过将方法静态化来解决,这没错!但这是基于你对以上几点有深刻的认知的前提下,万一哪天你的小弟在不知情的前提下,将 static 关键字删掉,问题又会出现了!
古往今来,使用匿名类已经带来够多的麻烦了,而匿名内部类,那简直更危险!因为无心的人根本不会注意到,他们通过一个匿名内部类将外部容器类的引用暴露了出去!
结论
SO,我们的结论是 没有蛀牙 永远不要使用双花括号初始化实例!!!
参考文章:Don’t be “Clever”: The Double Curly Braces Anti Pattern
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于