微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

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

如何解决Java“双大括号初始化”的效率?

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

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文件所需的磁盘空间了。

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


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

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,因此被无限期搁置。


这是我测试过的简单实验——ArrayList使用元素制作 1000 秒"Hello""World!"通过方法添加到它们add,使用两种方法

方法一:双括号初始化

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

方法2:实例化一个ArrayListandadd

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

请注意,初始化 1000ArrayList秒和 1000 个匿名内部类扩展ArrayList所用的时间是使用 来检查的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 ms。当然,要考虑定时器的分辨率,但它很可能在 15 毫秒以下。

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

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

解决方法

最佳答案提到了[Double BraceInitialization,其语法 :

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、可变参数方法、Google
Collections 和建议的 Java 7 Collection 文字来实现相同的效果。Scala、JRuby 和 Groovy 等较新的 JVM
语言也为列表构造提供了简洁的符号,并与 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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。