在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。


当前回答

There's generally nothing particularly inefficient about it. It doesn't generally matter to the JVM that you've made a subclass and added a constructor to it-- that's a normal, everyday thing to do in an object-oriented language. I can think of quite contrived cases where you could cause an inefficiency by doing this (e.g. you have a repeatedly-called method that ends up taking a mixture of different classes because of this subclass, whereas ordinary the class passed in would be totally predictable-- in the latter case, the JIT compiler could make optimisations that are not feasible in the first). But really, I think the cases where it'll matter are very contrived.

我更倾向于从你是否想用大量的匿名类“把事情弄得乱七八糟”的角度来看待这个问题。作为一个粗略的指南,可以考虑使用这个习惯用法,就像使用匿名类作为事件处理程序一样。

在(2)中,您处于对象的构造函数内部,因此“this”指的是您正在构造的对象。这和其他构造函数没有什么不同。

As for (3), that really depends on who's maintaining your code, I guess. If you don't know this in advance, then a benchmark that I would suggest using is "do you see this in the source code to the JDK?" (in this case, I don't recall seeing many anonymous initialisers, and certainly not in cases where that's the only content of the anonymous class). In most moderately sized projects, I'd argue you're really going to need your programmers to understand the JDK source at some point or other, so any syntax or idiom used there is "fair game". Beyond that, I'd say, train people on that syntax if you have control of who's maintaining the code, else comment or avoid.

其他回答

要创建集合,你可以使用varargs工厂方法来代替双大括号初始化:

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

谷歌Collections库有很多这样的方便方法,以及大量其他有用的功能。

至于这个习语的晦涩性,我一直在产品代码中遇到并使用它。我更关心的是那些被允许编写产品代码的习惯用法所迷惑的程序员。

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

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文件。

撇开效率不谈,我发现自己很少希望在单元测试之外创建声明式集合。我相信双大括号语法是非常可读的。

实现列表的声明式构造的另一种方法是使用数组。asList(T…)像这样:

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

当然,这种方法的局限性在于您不能控制要生成的列表的特定类型。

这将为每个成员调用add()。如果你能找到一种更有效的方法将项放入散列集中,那么就使用它。注意,内部类可能会生成垃圾,如果您对此很敏感的话。 在我看来,上下文似乎是new返回的对象,也就是HashSet。 如果你需要问…更有可能的是:在你之后的人会知道这一点吗?它容易理解和解释吗?如果两个问题你都能回答“是”,那就随便用吧。

加载许多类会在开始时增加几毫秒的时间。如果启动不是那么关键,你看看类启动后的效率,没有区别。

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

打印

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns