协慌网

登录 贡献 社区

Java“双支撑初始化” 的效率?

Java隐藏功能中 ,最佳答案提到了Double Brace Initialization ,它具有非常诱人的语法:

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 文字获得相同的效果。 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

答案

当我被匿名的内部类带走时,这就是问题所在:

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 CoinCollection Literals 提案遵循以下方针:

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

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

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

可悲的是,它没有进入 Java 7 和 8 并且无限期搁置。


实验

这是我测试过的简单实验 - 使用"Hello""World!"元素制作 1000 个ArrayList 。使用以下两种方法通过add方法添加到它们:

方法 1:双支撑初始化

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

方法 2:实例化ArrayListadd

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

请注意,使用System.currentTimeMillis检查初始化 1000 ArrayList和扩展ArrayList的 1000 个匿名内部类所用的时间,因此计时器的分辨率不是很高。在我的 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

可以看出,双支撑初始化具有大约 190ms 的显着执行时间。

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

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

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

到目前为止尚未指出此方法的一个属性是因为您创建内部类,所以在其范围内捕获整个包含类。这意味着只要你的 Set 处于活动状态,它就会保留一个指向包含实例的指针( this$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){

            }

        };
    }

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

除了语法相当不寻常而且不是真正的惯用语(当然,品味是值得商榷的)之外,您在应用程序中不必要地创建了两个重要问题, 我最近在这里更详细地讨论了这些问题

你创造了太多的匿名课程

每次使用双括号初始化时,都会生成一个新类。例如这个例子:

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

这对你的类加载器来说是一个相当大的开销 - 什么都不是!当然,如果你这样做一次,它将不需要太多的初始化时间。但是,如果你在整个企业应用程序中执行此操作 20,000 次... 所有堆内存只是为了一点 “语法糖”?

你可能会造成内存泄漏!

如果您使用上面的代码并从方法返回该映射,那么该方法的调用者可能会毫无疑问地保留非常繁重的资源,而这些资源无法进行垃圾回收。请考虑以下示例:

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的封闭实例的ReallyHeavyObject 。你可能不想冒这样的风险:

记忆泄漏就在这里

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

你可以假装 Java 有地图文字

为了回答你的实际问题,人们一直在使用这种语法假装 Java 有类似于现有数组文字的地图文字:

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

有些人可能会发现这种语法刺激。