在我刚刚接触如今这个产品的时候,我就在咱们的代码中接触到了对Double Brace Initialization的使用。那段代码用来初始化一个集合:html
1 final Set<String> exclusions = new HashSet<String>() {{ 2 add(‘Alice’); 3 add(‘Bob’); 4 add(‘Marine’); 5 }};
相信第一次看到这种使用方式的读者和我当时的感受同样:这是在作什么?固然,经过在函数add()的调用处加上断点,您就会了解到这其实是在使用add()函数向刚刚建立的集合exclusions中添加元素。ide
Double Brace Initialization简介函数
可为何咱们要用这种方式来初始化集合呢?做为比较,咱们先来看看一般状况下咱们所编写的具备相同内容集合的初始化代码:工具
1 final Set<String> exclusions = new HashSet<String>(); 2 exclusions.add(‘Alice’); 3 exclusions.add(‘Bob’); 4 exclusions.add(‘Marine’);
这些代码很繁冗,不是么?在编写这些代码的时候,咱们须要重复键入不少次exclusions。同时,这些代码在软件开发人员须要检查到底向该集合中添加了哪些元素的时候也很是恼人。反过来,使用Double Brace Initialization对集合进行初始化就十分简单明了:ui
1 final Set<String> exclusions = new HashSet<String>() {{ 2 add(‘Alice’); 3 add(‘Bob’); 4 add(‘Marine’); 5 }};
所以对于一个熟悉该使用方法的人来讲,Double Brace Initialization清晰简洁,代码可读性好维护性高,天然是初始化集合时的不二选择。而对于一个没有接触过该使用方法并且基础不是很牢靠的人来讲,Double Brace Initialization实在是有些晦涩难懂。spa
从晦涩到熟悉实际上很是简单,那就是了解它的工做原理。若是将上面的Double Brace Initialization示例稍微更改一下格式,相信您会看出一些端倪:调试
1 final Set<String> exclusions = new HashSet<String>() { 2 { 3 add(‘Alice’); 4 add(‘Bob’); 5 add(‘Marine’); 6 } 7 };
如今您能看出来到底Double Brace Initialization是如何运行的了吧?Double Brace Initialization一共包含两层花括号。外层的花括号实际上表示当前所建立的是一个派生自HashSet<String>的匿名类:code
1 final Set<String> exclusions = new HashSet<String>() { 2 // 匿名派生类的各个成员 3 };
而内层的花括号其实是在匿名派生类内部所声明的instance initializer:htm
1 final Set<String> exclusions = new HashSet<String>() { 2 { 3 // 因为匿名类中不能添加构造函数,所以这里的instance initializer 4 // 实际上等于构造函数,用来执行对当前匿名类实例的初始化 5 } 6 };
在经过Double Brace Initialization建立一个集合的时候,咱们所获得的其实是一个从集合类派生出的匿名类。在该匿名类初始化时,它内部所声明的instance initializer就会被执行,进而容许其中的函数调用add()来向刚刚建立好的集合添加元素。blog
其实Double Brace Initialization并不只仅局限于对集合类型的初始化。实际上,任何类型均可以经过它来执行预初始化:
1 NutritionFacts cocaCola = new NutritionFacts() {{ 2 setCalories(100); 3 setSodium(35); 4 setCarbohydrate(27); 5 }};
看到了吧。这和我另外一篇文章中所说起的Fluent Interface模式有殊途同归之妙。
Double Brace Initialization的优缺点
下一步,咱们就须要了解Double Brace Initialization的优缺点,从而更好地对它进行使用。
Double Brace Initialization的优势很是明显:对于熟悉该使用方法的人而言,它具备更好的可读性以及更好的维护性。
可是Double Brace Initialization一样具备一系列问题。最严重的可能就是Double Brace Initialization会致使内存泄露。在使用Double Brace Initialization的时候,咱们实际上建立了一个匿名类。匿名类有一个性质,那就是该匿名类实例将拥有一个包含它的类型的引用。若是咱们将该匿名类实例经过函数调用等方式传到该类型以外,那么对该匿名类的保持实际上会致使外层的类型没法被释放,进而形成内存泄露。
例如在Joshua Bloch版的Builder类实现中(详见这篇博文),咱们能够在build()函数中使用Double Brace Initialization来生成产品实例:
1 public class NutritionFacts { 2 …… 3 4 public static class Builder { 5 …… 6 public NutritionFacts build() { 7 return new NutritionFacts() {{ 8 setServingSize(100); 9 setServings(3); 10 …… 11 }}; 12 } 13 } 14 }
而在用户经过该Builder建立一个产品实例的时候,他将会使用以下代码:
1 NutritionFacts facts = new NutritionFacts.Builder.setXXX()….build();
上面的代码没有保持任何对NutritionFacts.Builder的引用,所以在执行完这段代码后,该段程序所实际使用的内存应该仅仅增长了一个NutritionFacts实例,不是么?答案是否认的。因为在build()函数中使用了Double Brace Initialization,所以在新建立的NutritionFacts实例中会包含一个NutritionFacts.Builder类型的引用。
另一个缺点则是破坏了equals()函数的语义。在为一个类型实现equals()函数的时候,咱们可能须要判断两个参与比较的类型是否一致:
1 @Override 2 public boolean equals(Object o) { 3 if (o != null && o.getClass().equals(getClass())) { 4 …… 5 } 6 7 return false; 8 }
这种实现有必定的争议。争议点主要在于Joshua Bloch在Effective Java的Item 8中说它违反了里氏替换原则。反驳这种观点的人则主要认为维护equals()函数返回结果正确性的责任须要由派生类来保证。并且从语义上来讲,若是两个类的类型都不同,那么它们之间还彼此相等自己就是一件荒谬的事情。所以在某些类库的实现中,它们都经过检查类型的方式强行要求参与比较的两个实例的类型须要是一致的。
而在使用Double Brace Initialization的时候,咱们则建立了一个从目标类型派生的匿名类。就以刚刚所展现的build()函数为例:
1 public class NutritionFacts { 2 …… 3 4 public static class Builder { 5 …… 6 public NutritionFacts build() { 7 return new NutritionFacts() {{ 8 setServingSize(100); 9 setServings(3); 10 …… 11 }}; 12 } 13 } 14 }
在build()函数中,咱们所建立的其实是从NutritionFacts派生的匿名类。若是咱们在该段代码以后添加一个断点,咱们就能够从调试功能中看到该段代码所建立实例的实际类型是NutritionFacts$1。所以,若是NutritionFacts的equals()函数内部实现判断了参与比较的两个实例所具备的类型是否一致,那么咱们刚刚经过Double Brace Initialization所获得的NutritionFacts$1类型实例将确定与其它的NutritionFacts实例不相等。
好,既然咱们刚刚提到了匿名类在调试器中的表示,那么咱们就须要慎重地考虑这个问题。缘由很简单:在较为复杂的Double Brace Initialization的使用中,这些匿名类的表示会很是难以阅读。就如下面的代码为例:
1 Map<String, Object> characterInfo = new HashMap<String, Object>() {{ 2 put("firstName", "John"); 3 put("lastName", "Smith"); 4 put("children", new HashSet<HashMap<String, Object>>() {{ 5 add(new HashMap<String, Object>() {{ 6 put("firstName", "Alice"); 7 put("lastName", "Smith"); 8 }}); 9 add(new HashMap<String, Object>() {{ 10 put("firstName", "George"); 11 put("lastName", "Smith"); 12 }}); 13 }}); 14 }};
而在使用调试器进行调试的时候,您会看到如下一系列类型:
Sample.class
Sample$1.class
Sample$1$1.class
Sample$1$1$1.class
Sample$1$1$2.class
在查看这些数据的时候,咱们经常没法直接理解这些数据到底表明的是什么。所以软件开发人员经常须要查看它们的基类究竟是什么,并根据调用栈去查找这些数据的初始化逻辑,才能了解这些数据所具备的真正含义。在这种状况下,Double Brace Initialization所提供的再也不是较高的维护性,反而变成了维护的负担。
同时因为Double Brace Initialization须要建立一个目标类型的派生类,所以咱们不能在一个由final修饰的类型上使用Double Brace Initialization。
并且值得一提的是,在某些IDE中,Double Brace Initialization的格式实际上显得很是奇怪。这使得Double Brace Initialization丧失了其最大优点。
并且在使用Double Brace Initialization以前,咱们首先要问本身:咱们是否在使用一系列常量来初始化集合?若是是,那么为何要将数据和应用逻辑混合在一块儿?若是这两个问题中的任意一个是否认的,那么就表示咱们应该使用独立的文件来记录应用所须要的数据,如*.properties文件等,并在应用运行时加载这些数据。
适当地使用Double Brace Initialization
能够说,Double Brace Initialization虽然在表意上具备突出优点,它的缺点也很是明显。所以软件开发人员须要谨慎地对它进行使用。
在前面的介绍中咱们已经看到,Double Brace Initialization最大的问题就是在表达复杂数据的时候反而会增长的维护成本,在equals()函数方面不清晰的语义以及潜在的内存泄露。
第一个缺点很是容易避免,那就是在建立一个复杂的数据集合时,咱们再也不考虑使用Double Brace Initialization,而是将这些数据存储在一个专门的数据文件中,并在应用运行时加载。
然后两个缺点则能够经过限制该部分数据的使用范围来完成。
那在须要初始化复杂数据的时候,咱们应该怎么办?为此业内也提出了一系列解决方案。这些方案不只能够提升代码的表意性,还能够避免因为使用Double Brace Initialization所引入的一系列问题。
最多见的一种解决方案就是使用第三方类库。例如由Apache Commons类库提供的ArrayUtils.toMap()函数就提供了一种很是清晰的建立Map的实现:
1 Map<Integer, String> map = (Map) ArrayUtils.toMap(new Object[][] { 2 {1, "one"}, 3 {2, "two"}, 4 {3, "three"} 5 });
若是说您不喜欢引入第三方类库,您也能够经过建立一个工具函数来完成相似的事情:
Map<Integer, String> map = Utils.toMap(new Object[][] { {1, "one"}, {2, "two"}, {3, "three"} }); public Map<Integer, String> toMap(Object[][] mapData) { …… }
转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/4593962.html
商业转载请事先与我联系:silverfox715@sina.com