Java“双括号初始化”的效率?

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”值。非常整洁。

总的来说,DBI给我的印象是一种智力上的好奇心。Coobird和其他人指出,你可以使用Arrays.asList,varargs方法,Google Collections和提议的Java 7 Collection literals达到同样的效果。较新的JVM语言,如Scala,JRuby和Groovy也为列表构建提供了简洁的符号,并且可以与Java很好地互操作。鉴于 DBI 会使类路径变得混乱,稍微减慢类加载速度,并使代码更加晦涩难懂,我可能会回避它。但是,我计划将此强加给一个刚刚获得SCJP的朋友,并且喜欢关于Java语义的良好竞争!;-)谢谢大家!

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

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

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

这肯定是要走的路。如果您坚持使用早期版本,请查看Google Collections的ImmutableSet


答案 1

当我对匿名的内部类过于着迷时,这就是问题所在:

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

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

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

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

使用双括号初始化时似乎有一点开销,因此过于过分可能不是一个好主意。但正如Eddie在评论中指出的那样,不可能绝对确定其影响。


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

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 CoinCollection Literals提案大致如下:

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

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

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

可悲的是,它既没有进入Java 7也没有进入Java 8,而是被无限期地搁置了。


实验

这是我测试过的简单实验 - 用元素制作1000个,然后通过方法添加,使用两种方法:ArrayList"Hello""World!"add

方法 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 秒和 1000 个匿名内部类扩展所花费的时间是使用 检查的,因此计时器没有很高的分辨率。在我的Windows系统上,分辨率约为15-16毫秒。ArrayListArrayListSystem.currentTimeMillis

两个测试的 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 毫秒。

同时,初始化执行时间为0 ms。当然,应该考虑计时器分辨率,但很可能在15 ms以下。ArrayList

因此,这两种方法的执行时间似乎存在明显差异。看起来这两种初始化方法确实存在一些开销。

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


答案 2

到目前为止尚未指出此方法的一个属性是,由于创建了内部类,因此将在其范围内捕获整个包含类。这意味着,只要您的 Set 处于活动状态,它就会保留一个指向包含实例 () 的指针,并防止该实例被垃圾回收,这可能是一个问题。this$0

这一点,以及即使常规的HashSet可以正常工作(甚至更好)也首先创建新类的事实使我不想使用这种构造(即使我非常渴望语法糖)。

第二个问题:新的 HashSet 必须是实例初始值设定项中使用的“this”...任何人都可以阐明该机制吗?我天真地以为“this”指的是初始化“flavors”的对象。

这就是内部类的工作方式。它们有自己的 ,但它们也有指向父实例的指针,以便您也可以在包含对象上调用方法。在命名冲突的情况下,内部类(在你的例子中是HashSet)优先,但你可以用类名前缀“this”来获取外部方法。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){

            }

        };
    }

推荐