Java“双Brace初始化”的效率?

在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,可变参数方法,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!");
    }
};

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


约书亚布洛赫的收藏文学项目硬币的字面意思是:

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

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

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

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


实验

下面是我测试过的简单实验 - 使1000个ArrayList具有元素"Hello""World!" 通过add方法添加到它们中,使用两种方法:

方法1:双Brace初始化

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 s和1000匿名内部类扩展ArrayList所用的时间,因此计时器的分辨率并不高。 在我的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 ms。

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

所以,这两种方法的执行时间似乎有明显的差异。 它似乎确实在两种初始化方法中确实存在一些开销。

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


这种方法迄今尚未被指出的一个属性是,因为你创建了内部类,整个包含的类被捕获在它的范围内。 这意味着只要你的Set是活着的,它就会保留一个指向包含实例( this$0 )的指针,并保持垃圾收集,这可能是一个问题。

这一点,以及即使一个普通的HashSet工作得很好(甚至更好),一个新类创建的事实,使我不想使用这个构造(即使我真的渴望语法糖)。

第二个问题:新的HashSet必须是实例初始化程序中使用的“this”......任何人都可以阐明该机制? 我会天真地期待“this”引用初始化“flavor”的对象。

这就是内部类的工作方式。 他们获得了他们自己的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){

            }

        };
    }

参加以下测试课程:

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

您也可以对构造函数的用法(JLS 8.6实例初始化程序)使用类似的语法(没有“ static ”一词),尽管我从未在生产代码中看到过它的用法。 这是众所周知的。

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

出于初始化的目的,我会说没有任何开销(或者很小,以至于可以忽略)。 但是,每种flavors使用都不会针对HashSet ,而是针对MyHashSet 。 这可能会有一小部分(而且可能可以忽略不计)。 但是,再次担心之前,我会对其进行描述。

同样,对于你的问题#2,上面的代码是双重大括号初始化的逻辑和显式等价物,并且它明确了“ this ”引用的位置:对于扩展HashSet的内部类。

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

链接地址: http://www.djcxy.com/p/2331.html

上一篇: Efficiency of Java "Double Brace Initialization"?

下一篇: Are the Heap, the Stack and the Data segment on the same assembly program?