在Java的隐藏特性中,最上面的答案提到了双大括号初始化,语法非常诱人:

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

这个习惯用法创建了一个匿名的内部类,其中只有一个实例初始化式,它“可以使用任何[…]]方法”。

主要问题:这真的像听起来那么低效吗?它的使用应该仅限于一次性的初始化吗?(当然还有炫耀!)

第二个问题:新的HashSet必须是实例初始化器中使用的“this”…有人能解释一下原理吗?

第三个问题:在产品代码中使用这个习语是否过于晦涩?

非常非常好的回答,谢谢大家。关于问题(3),人们认为语法应该清楚(尽管我建议偶尔注释一下,特别是如果您的代码将传递给可能不熟悉它的开发人员)。

对于问题(1),生成的代码应该快速运行。额外的.class文件确实会导致jar文件混乱,并略微减慢程序启动速度(感谢@coobird测量)。@Thilo指出,垃圾收集可能会受到影响,在某些情况下,额外加载类的内存成本可能是一个因素。

问题(2)对我来说是最有趣的。如果我理解了答案,DBI中发生的事情是匿名内部类扩展了new操作符所构造的对象的类,因此有一个“this”值引用正在构造的实例。非常整洁。

Overall, DBI strikes me as something of an intellectual curiousity. Coobird and others point out you can achieve the same effect with Arrays.asList, varargs methods, Google Collections, and the proposed Java 7 Collection literals. Newer JVM languages like Scala, JRuby, and Groovy also offer concise notations for list construction, and interoperate well with Java. Given that DBI clutters up the classpath, slows down class loading a bit, and makes the code a tad more obscure, I'd probably shy away from it. However, I plan to spring this on a friend who's just gotten his SCJP and loves good natured jousts about Java semantics! ;-) Thanks everyone!

7/2017: Baeldung很好地总结了双大括号初始化,并认为这是一种反模式。

2017年12月12日:@Basil Bourque指出,在新的Java 9中,你可以说:

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");

这肯定是正确的选择。如果你还在使用以前的版本,可以看看谷歌Collections的ImmutableSet。


当前回答

虽然这种语法很方便,但它也添加了大量的$0引用,因为这些引用是嵌套的,并且很难对初始化器进行步进调试,除非在每个初始化器上设置断点。出于这个原因,我只建议在普通的设置器,特别是设置为常量,以及匿名子类无关紧要的地方使用这个方法(比如不涉及序列化)。

其他回答

到目前为止还没有指出的这种方法的一个特性是,因为创建了内部类,所以整个包含类都在其作用域中被捕获。这意味着只要你的Set是活的,它将保留一个指向包含实例的指针(这个$0),并防止它被垃圾收集,这可能是一个问题。

这一点,以及尽管常规HashSet可以很好地工作(甚至更好),但首先创建了一个新类的事实,使我不想使用这个构造(尽管我非常渴望语法糖)。

第二个问题:新的HashSet必须是实例初始化器中使用的“this”…有人能解释一下原理吗?我天真地以为“this”指的是初始化“flavors”的对象。

这就是内部类的工作方式。它们有自己的this,但是它们也有指向父实例的指针,所以你也可以调用包含对象的方法。在命名冲突的情况下,内部类(在您的例子中是HashSet)优先,但是您可以在类名前面加上“this”来获得外部方法。

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

为了清楚地了解正在创建的匿名子类,您也可以在其中定义方法。例如重写HashSet.add()

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }

每当有人使用双大括号初始化时,就会有一只小猫被杀死。

除了语法相当不寻常且不是真正的惯用(当然,品味是有争议的)之外,您还不必要地在应用程序中创建了两个重大问题,我最近在博客中对此进行了更详细的讨论。

1. 你创建了太多的匿名类

每次使用双大括号初始化都会创建一个新类。例如这个例子:

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");
        }});
    }});
}};

... 将产生这些类:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

这对您的类加载器来说是相当大的开销——毫无意义!当然,如果你只做一次,就不会花费太多初始化时间。但是如果你在整个企业应用程序中做了20000次这样的事情……那么多内存只是为了一点“语法糖”?

2. 您可能会造成内存泄漏!

如果您使用上述代码并从一个方法返回该映射,那么该方法的调用者可能会毫无疑问地持有无法被垃圾收集的非常重的资源。考虑下面的例子:

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    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;
    }
}

返回的Map现在将包含对ReallyHeavyObject的封装实例的引用。你可能不想冒这个险:

图片来自http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/

3.你可以假装Java有地图字面量

为了回答你的实际问题,人们一直在使用这种语法来假装Java有类似map字面量的东西,类似于现有的数组字面量:

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

有些人可能会觉得这在语法上很刺激。

双大括号初始化是一种不必要的攻击,可能会导致内存泄漏和其他问题

没有合理的理由使用这种“伎俩”。Guava提供了很好的不可变集合,包括静态工厂和构建器,允许您在以干净、可读和安全的语法声明的地方填充集合。

问题中的例子是:

Set<String> flavors = ImmutableSet.of(
    "vanilla", "strawberry", "chocolate", "butter pecan");

这不仅更短,更容易阅读,而且避免了其他答案中描述的双括号模式的许多问题。当然,它的性能类似于直接构造的HashMap,但它很危险且容易出错,而且还有更好的选择。

任何时候,当你发现自己在考虑双括号初始化时,你都应该重新检查你的api或引入新的api来正确地解决这个问题,而不是利用语法技巧。

Error-Prone现在标记此反模式。

执行以下测试类:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

然后反编译类文件,我看到:

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

在我看来,这并不是非常低效。如果我担心这样的东西的性能,我会分析它。上面的代码回答了你的问题#2:你在内部类的隐式构造函数(和实例初始化式)中,所以“this”指的是这个内部类。

是的,这个语法是模糊的,但是注释可以阐明模糊的语法用法。为了澄清语法,大多数人都熟悉静态初始化块(JLS 8.7 static Initializers):

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

您还可以使用类似的语法(没有“static”这个词)来使用构造函数(JLS 8.6实例初始化器),尽管我从未在生产代码中看到过这种用法。这一点很少为人所知。

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

如果没有默认构造函数,则编译器将{和}之间的代码块转换为构造函数。考虑到这一点,解开双括号代码:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

最里面的大括号之间的代码块被编译器转换为构造函数。最外面的花括号分隔了匿名内部类。这是使一切都非匿名的最后一步:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

对于初始化的目的,我认为没有任何开销(或者小到可以忽略不计)。但是,每次使用flavor都不会违背HashSet,而是违背MyHashSet。这可能有一个很小的开销(很可能可以忽略不计)。但是,在我担心之前,我会先分析一下。

同样,对于您的问题#2,上面的代码在逻辑上和显式地等价于双大括号初始化,并且它使“this”指的地方很明显:指向扩展HashSet的内部类。

如果您对实例初始化器的详细信息有疑问,请查看JLS文档中的详细信息。

当我过于沉迷于匿名内部类时,有一个问题:

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

这些都是我在制作一个简单应用程序时生成的类,并使用了大量的匿名内部类——每个类将被编译成一个单独的类文件。

如前所述,“双大括号初始化”是一个带有实例初始化块的匿名内部类,这意味着每次“初始化”都会创建一个新类,所有这些通常都是为了创建单个对象。

考虑到Java虚拟机在使用这些类时需要读取所有这些类,这可能会导致字节码验证过程等花费一些时间。更不用说存储所有这些类文件所需的磁盘空间的增加了。

在使用双大括号初始化时,似乎会有一些开销,所以太过分可能不是一个好主意。但正如艾迪在评论中指出的那样,不可能完全确定其影响。


仅供参考,双大括号初始化如下:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

它看起来像Java的一个“隐藏”特性,但它只是一个重写:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

它基本上是一个实例初始化块,是匿名内部类的一部分。


Joshua Bloch为Project Coin设计的Collection Literals方案是这样的:

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

遗憾的是,它既没有出现在Java 7中,也没有出现在Java 8中,而是被无限期地搁置了。


实验

下面是我测试过的一个简单的实验——通过add方法将“Hello”和“World!”元素添加到1000个数组列表中,使用以下两种方法:

方法1:双大括号初始化

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

方法2:实例化一个数组列表并添加

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

我创建了一个简单的程序,写出一个Java源文件,使用以下两种方法执行1000个初始化:

测试1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

测试2:

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

请注意,初始化1000个ArrayList和扩展ArrayList的1000个匿名内部类所消耗的时间是使用System检查的。currentTimeMillis,所以定时器的分辨率不是很高。在我的Windows系统上,分辨率大约是15-16毫秒。

这两项试验进行了10次,结果如下:

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

可以看到,双大括号初始化的执行时间大约为190毫秒。

同时,ArrayList初始化执行时间为0毫秒。当然,应该考虑定时器分辨率,但它很可能低于15毫秒。

因此,这两个方法的执行时间似乎有明显的差异。这两个初始化方法似乎确实有一些开销。

是的,通过编译Test1双大括号初始化测试程序生成了1000个.class文件。