在我刚刚接触现在这个产品的时候,我就在我们的代码中接触到了对Double Brace Initialization的使用。那段代码用来初始化一个集合:

 final Set<String> exclusions = new HashSet<String>() {{
add(‘Alice’);
add(‘Bob’);
add(‘Marine’);
}};

  相信第一次看到这种使用方式的读者和我当时的感觉一样:这是在做什么?当然,通过在函数add()的调用处加上断点,您就会了解到这实际上是在使用add()函数向刚刚创建的集合exclusions中添加元素。

Double Brace Initialization简介

  可为什么我们要用这种方式来初始化集合呢?作为比较,我们先来看看通常情况下我们所编写的具有相同内容集合的初始化代码:

 final Set<String> exclusions = new HashSet<String>();
exclusions.add(‘Alice’);
exclusions.add(‘Bob’);
exclusions.add(‘Marine’);

  这些代码很繁冗,不是么?在编写这些代码的时候,我们需要重复键入很多次exclusions。同时,这些代码在软件开发人员需要检查到底向该集合中添加了哪些元素的时候也非常恼人。反过来,使用Double Brace Initialization对集合进行初始化就十分简单明了:

 final Set<String> exclusions = new HashSet<String>() {{
add(‘Alice’);
add(‘Bob’);
add(‘Marine’);
}};

  因此对于一个熟悉该使用方法的人来说,Double Brace Initialization清晰简洁,代码可读性好维护性高,自然是初始化集合时的不二选择。而对于一个没有接触过该使用方法而且基础不是很牢靠的人来说,Double Brace Initialization实在是有些晦涩难懂。

  从晦涩到熟悉实际上非常简单,那就是了解它的工作原理。如果将上面的Double Brace Initialization示例稍微更改一下格式,相信您会看出一些端倪:

 final Set<String> exclusions = new HashSet<String>() {
{
add(‘Alice’);
add(‘Bob’);
add(‘Marine’);
}
};

  现在您能看出来到底Double Brace Initialization是如何运行的了吧?Double Brace Initialization一共包含两层花括号。外层的花括号实际上表示当前所创建的是一个派生自HashSet<String>的匿名类:

 final Set<String> exclusions = new HashSet<String>() {
// 匿名派生类的各个成员
};

  而内层的花括号实际上是在匿名派生类内部所声明的instance initializer:

 final Set<String> exclusions = new HashSet<String>() {
{
// 由于匿名类中不能添加构造函数,因此这里的instance initializer
// 实际上等于构造函数,用来执行对当前匿名类实例的初始化
}
};

  在通过Double Brace Initialization创建一个集合的时候,我们所得到的实际上是一个从集合类派生出的匿名类。在该匿名类初始化时,它内部所声明的instance initializer就会被执行,进而允许其中的函数调用add()来向刚刚创建好的集合添加元素。

  其实Double Brace Initialization并不仅仅局限于对集合类型的初始化。实际上,任何类型都可以通过它来执行预初始化:

 NutritionFacts cocaCola = new NutritionFacts() {{
setCalories(100);
setSodium(35);
setCarbohydrate(27);
}};

  看到了吧。这和我另一篇文章中所提及的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来生成产品实例:

 public class NutritionFacts {
…… public static class Builder {
……
public NutritionFacts build() {
return new NutritionFacts() {{
setServingSize(100);
setServings(3);
……
}};
}
}
}

  而在用户通过该Builder创建一个产品实例的时候,他将会使用如下代码:

 NutritionFacts facts = new NutritionFacts.Builder.setXXX()….build();

  上面的代码没有保持任何对NutritionFacts.Builder的引用,因此在执行完这段代码后,该段程序所实际使用的内存应该仅仅增加了一个NutritionFacts实例,不是么?答案是否定的。由于在build()函数中使用了Double Brace Initialization,因此在新创建的NutritionFacts实例中会包含一个NutritionFacts.Builder类型的引用。

  另外一个缺点则是破坏了equals()函数的语义。在为一个类型实现equals()函数的时候,我们可能需要判断两个参与比较的类型是否一致:

 @Override
public boolean equals(Object o) {
if (o != null && o.getClass().equals(getClass())) {
……
} return false;
}

  这种实现有一定的争议。争议点主要在于Joshua Bloch在Effective Java的Item 8中说它违反了里氏替换原则。反驳这种观点的人则主要认为维护equals()函数返回结果正确性的责任需要由派生类来保证。而且从语义上来说,如果两个类的类型都不一样,那么它们之间还彼此相等本身就是一件荒谬的事情。因此在某些类库的实现中,它们都通过检查类型的方式强行要求参与比较的两个实例的类型需要是一致的。

  而在使用Double Brace Initialization的时候,我们则创建了一个从目标类型派生的匿名类。就以刚刚所展示的build()函数为例:

 public class NutritionFacts {
…… public static class Builder {
……
public NutritionFacts build() {
return new NutritionFacts() {{
setServingSize(100);
setServings(3);
……
}};
}
}
}

  在build()函数中,我们所创建的实际上是从NutritionFacts派生的匿名类。如果我们在该段代码之后添加一个断点,我们就可以从调试功能中看到该段代码所创建实例的实际类型是NutritionFacts$1。因此,如果NutritionFacts的equals()函数内部实现判断了参与比较的两个实例所具有的类型是否一致,那么我们刚刚通过Double Brace Initialization所得到的NutritionFacts$1类型实例将肯定与其它的NutritionFacts实例不相等。

  好,既然我们刚刚提到了匿名类在调试器中的表示,那么我们就需要慎重地考虑这个问题。原因很简单:在较为复杂的Double Brace Initialization的使用中,这些匿名类的表示会非常难以阅读。就以下面的代码为例:

 Map<String, Object> characterInfo = new HashMap<String, Object>() {{
put("firstName", "John");
put("lastName", "Smith");
put("children", new HashSet<HashMap<String, Object>>() {{
add(new HashMap<String, Object>() {{
put("firstName", "Alice");
put("lastName", "Smith");
}});
add(new HashMap<String, Object>() {{
put("firstName", "George");
put("lastName", "Smith");
}});
}});
}};

  而在使用调试器进行调试的时候,您会看到以下一系列类型:

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的实现:

 Map<Integer, String> map = (Map) ArrayUtils.toMap(new Object[][] {
{1, "one"},
{2, "two"},
{3, "three"}
});

  如果说您不喜欢引入第三方类库,您也可以通过创建一个工具函数来完成类似的事情:

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

Java:Double Brace Initialization的更多相关文章

  1. Java:双括号初始化 /匿名内部类初始化法

    偶然见到一种初始化方式,感到十分新奇: //新建一个列表并赋初值A.B.C ArrayList<String> list = new ArrayList<String>() { ...

  2. (后台)Java:对double值进行四舍五入,保留两位小数的几种方法

    mport java.text.DecimalFormat; DecimalFormat df = new DecimalFormat("######0.00"); double ...

  3. Java:利用BigDecimal类巧妙处理Double类型精度丢失

    目录 本篇要点 经典问题:浮点数精度丢失 十进制整数如何转化为二进制整数? 十进制小数如何转化为二进制数? 如何用BigDecimal解决double精度问题? new BigDecimal(doub ...

  4. Kotlin中变量不同于Java: var 对val(KAD 02)

    原文标题:Variables in Kotlin, differences with Java. var vs val (KAD 02) 作者:Antonio Leiva 时间:Nov 28, 201 ...

  5. java使double保留两位小数的多方法 java保留两位小数

    这篇文章主要介绍了java使double类型保留两位小数的方法,大家参考使用吧 复制代码 代码如下: mport java.text.DecimalFormat; DecimalFormat    d ...

  6. Java:类与继承

    Java:类与继承 对于面向对象的程序设计语言来说,类毫无疑问是其最重要的基础.抽象.封装.继承.多态这四大特性都离不开类,只有存在类,才能体现面向对象编程的特点,今天我们就来了解一些类与继承的相关知 ...

  7. 深入理解Java:注解

    注解作用:每当你创建描述符性质的类或者接口时,一旦其中包含重复性的工作,就可以考虑使用注解来简化与自动化该过程. Java提供了四种元注解,专门负责新注解的创建工作. 元注解 元注解的作用就是负责注解 ...

  8. java中double变量保留小数问题

    (转载自玄影池扁舟) 做java项目的时候可能经常会遇到double类型变量保留小数的问题,下面便把我的经验做个简短的总结: java中double类型变量保留小数问题大体分两种情况: (一):小数点 ...

  9. 关于java中Double类型的运算精度问题

    标题     在Java中实现浮点数的精确计算    AYellow(原作) 修改    关键字     Java 浮点数 精确计算   问题的提出:如果我们编译运行下面这个程序会看到什么?publi ...

随机推荐

  1. Hadoop 中利用 mapreduce 读写 mysql 数据

    Hadoop 中利用 mapreduce 读写 mysql 数据   有时候我们在项目中会遇到输入结果集很大,但是输出结果很小,比如一些 pv.uv 数据,然后为了实时查询的需求,或者一些 OLAP ...

  2. SQL Server 大数据搬迁之文件组备份还原实战

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 解决方案(Solution) 搬迁步骤(Procedure) 搬迁脚本(SQL Codes) ...

  3. VM(虚拟机安装win7 提示 :units specified don't exist, SHSUCDX can't install)解决方法

    改成IDE的模式

  4. Hyper-V 激活Windows系统重启后黑屏的解决方法 + 激活方法

    异常处理汇总-服 务 器 http://www.cnblogs.com/dunitian/p/4522983.html 服务器相关的知识点:http://www.cnblogs.com/dunitia ...

  5. 【翻译】MongoDB指南/聚合——聚合管道

    [原文地址]https://docs.mongodb.com/manual/ 聚合 聚合操作处理数据记录并返回计算后的结果.聚合操作将多个文档分组,并能对已分组的数据执行一系列操作而返回单一结果.Mo ...

  6. [.NET] 利用 async & await 进行异步 IO 操作

    利用 async & await 进行异步 IO 操作 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6082673.html  序 上次,博主 ...

  7. iOS开发之Masonry框架源码深度解析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使用起来更为简洁.Masonry简化了NSLayoutConstraint的使用方式,让 ...

  8. c#多线程

    一.使用线程的理由 1.可以使用线程将代码同其他代码隔离,提高应用程序的可靠性. 2.可以使用线程来简化编码. 3.可以使用线程来实现并发执行. 二.基本知识 1.进程与线程:进程作为操作系统执行程序 ...

  9. git-2.10.2-64-bit介绍&&git下载&&git安装教程

    Git介绍 分布式:Git系统是一个分布式的系统,是用来保存工程源代码历史状态的命令行工具. 保存点:Git的保存点可以追踪源码中的文件, 并能得到某一个时间点上的整个工程项目的状态:可以在该保存点将 ...

  10. 破解SQLServer for Linux预览版的3.5GB内存限制 (RHEL篇)

    微软发布了SQLServer for Linux,但是安装竟然需要3.5GB内存,这让大部分云主机用户都没办法尝试这个新东西 这篇我将讲解如何破解这个内存限制 要看关键的可以直接跳到第6步,只需要替换 ...