在我刚刚接触现在这个产品的时候,我就在我们的代码中接触到了对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. 了解PHP中的Array数组和foreach

    1. 了解数组 PHP 中的数组实际上是一个有序映射.映射是一种把 values 关联到 keys 的类型.详细的解释可参见:PHP.net中的Array数组    . 2.例子:一般的数组 这里,我 ...

  2. category中重写方法?

    问:可以在category中重写方法吗? 答:代码上可以实现 在category中重写方法,但在实际开发中,不建议这样做.如果确实需要重写原有方法也建议使用子类进行重写. category是为了更方便 ...

  3. 算法与数据结构(十五) 归并排序(Swift 3.0版)

    上篇博客我们主要聊了堆排序的相关内容,本篇博客,我们就来聊一下归并排序的相关内容.归并排序主要用了分治法的思想,在归并排序中,将我们需要排序的数组进行拆分,将其拆分的足够小.当拆分的数组中只有一个元素 ...

  4. CRL快速开发框架系列教程六(分布式缓存解决方案)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  5. GPG终极指南(加密/签名)

    我们平时都听过非对称加密,公钥和私钥,签名验证,但这些证书都是怎么得到的呢?本篇文章会解答这些问题. 背景介绍 加密的一个简单但又实用的任务就是发送加密电子邮件.多年来,为电子邮件进行加密的标准一直是 ...

  6. .Net语言 APP开发平台——Smobiler学习日志:手机应用的TextTabBar快速实现方式

    参考页面: http://www.yuanjiaocheng.net/webapi/create-crud-api-1-put.html http://www.yuanjiaocheng.net/we ...

  7. org.jboss.deployment.DeploymentException: Trying to install an already registered mbean: jboss.jca:service=LocalTxCM,name=egmasDS

    17:34:37,235 INFO [Http11Protocol] Starting Coyote HTTP/1.1 on http-0.0.0.0-8080 17:34:37,281 INFO [ ...

  8. C# Entity Framework并发处理

    原网站:C# Entity Framework并发处理 在软件开发过程中,并发控制是确保及时纠正由并发操作导致的错误的一种机制.从 ADO.NET 到 LINQ to SQL 再到如今的 ADO.NE ...

  9. 微信小程序开发—快速掌握组件及API的方法

    微信小程序框架为开发者提供了一系列的组件和API接口. 组件主要完成小程序的视图部分,例如文字.图片显示.API主要完成逻辑功能,例如网络请求.数据存储.音视频播放控制,以及微信开放的微信登录.微信支 ...

  10. 浅谈iptables 入站 出站以及NAT实例

    --------------本文是自己工作上的笔记总结,适合的可以直接拿去用,不适合的,适当修改即可!--------------- iptbales默认ACCEPT策略,也称通策略,这种情况下可以做 ...