首页 > 编程语言 >Java:Double Brace Initialization

Java:Double Brace Initialization

时间:2023-05-29 13:05:13浏览次数:33  
标签:Java Double Initialization add new NutritionFacts Brace


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



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



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

 

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对集合进行初始化就十分简单明了:



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



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

  从晦涩到熟悉实际上非常简单,那就是了解它的工作原理。如果将上面的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>的匿名类:



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



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



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



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

  其实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) {
    ……
}




 


标签:Java,Double,Initialization,add,new,NutritionFacts,Brace
From: https://blog.51cto.com/u_16131764/6370099

相关文章

  • java编码转换过程
    java编码转换过程我们总是用一个java类文件和用户进行最直接的交互(输入、输出),这些交互内容包含的文字可能会包含中文。无论这些java类是与数据库交互,还是与前端页面交互,他们的生命周期总是这样的:1、程序员在操作系统上通过编辑器编写程序代码并且以.java的格式保存操作系统中,这些文......
  • javaWeb中的编码解码
    在上篇博客中LZ介绍了前面两种场景(IO、内存)中的Java编码解码操作,其实在这两种场景中我们只需要在编码解码过程中设置正确的编码解码方式一般而言是不会出现乱码的。对于我们从事java开发的人而言,其实最容易也是产生乱码最多的地方就是web部分。首先我们来看在javaWeb中有哪些地方存......
  • java是如何编码解码的
    编码&解码在java中主要有四个场景需要进行编码解码操作:1:I/O操作2:内存3:数据库4:javaWeb下面主要介绍前面两种场景,数据库部分只要设置正确编码格式就不会有什么问题,javaWeb场景过多需要了解URL、get、POST的编码,servlet的解码,所以javaWeb场景下节LZ介绍。I/O操作在前面LZ就提过乱码问......
  • Java中的Copy-On-Write容器
    Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,......
  • java线程池
    1.JDK中的Executor框架是基于生产者-消费者模式的线程池,提交任务的线程是生产者,执行任务的线程是消费者。Executor线程池可以用于异步任务执行,而且支持很多不同类型任务执行策略,同时为任务提交和任务执行之间的解耦提供了标准方法。Executor线程池支持如下三种线程执行策略:(1).顺序......
  • javascript常用正则表达式
    javascript身份证号验证正则1.//这个可以验证15位和18位的身份证,并且包含生日和校验位的验证。2.//如果有兴趣,还可以加上身份证所在地的验证,就是前6位有些数字合法有些数字不合法。3.4.function5.num=num.toUpperCase();6.//身份证号码为15位或者18......
  • Javascript编程风格
     所谓"编程风格"(programmingstyle),指的是编写代码的样式规则。不同的程序员,往往有不同的编程风格。 有人说,编译器的规范叫做"语法规则"(grammar),这是程序员必须遵守的;而编译器忽略的部分,就叫"编程风格"(programmingstyle),这是程序员可以自由选择的。这种说法不完全正确,程序员固然可......
  • java中fail-fast 和 fail-safe的区别
     在我们详细讨论这两种机制的区别之前,首先得先了解并发修改。1.什么是并发修改?当一个或多个线程正在遍历一个集合Collection,此时另一个线程修改了这个集合的内容(添加,删除或者修改)。这就是并发修改。 2.什么是fail-fast机制?fail-fast机制在遍历一个集合时,当集合结构被修改,会抛......
  • java socket
         对于JavaSocket编程而言,有两个概念,一个是ServerSocket,一个是Socket。服务端和客户端之间通过Socket建立连接,之后它们就可以进行通信了。首先ServerSocket将在服务端监听某个端口,当发现客户端有Socket来试图连接它时,它会accept该Socket的连接请求,同时在服务端建立一个......
  • Java中的堆和栈的区别
    当一个人开始学习Java或者其他编程语言的时候,会接触到堆和栈,由于一开始没有明确清晰的说明解释,很多人会产生很多疑问,什么是堆,什么是栈,堆和栈有什么区别?更糟糕的是,Java中存在栈这样一个后进先出(LastInFirstOut)的顺序的数据结构,这就是java.util.Stack。这种情况下,不免让很多人更加......