首页 文章

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

提问于
浏览
717

Hidden Features of Java中,最佳答案提到Double Brace Initialization,语法非常诱人:

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

这个成语创建了一个匿名内部类,其中只包含一个实例初始化程序,“可以使用包含作用域中的任何[...]方法” .

主要问题:听起来是 inefficient 吗?它的使用是否应限于一次性初始化? (当然炫耀!)

第二个问题:新的HashSet必须是实例初始化程序中使用的“this”...任何人都可以阐明机制吗?

第三个问题:这个成语是否也在 生产环境 代码中使用 obscure

Summary: 非常非常好的答案,谢谢大家 . 在问题(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 has a good summary的双括号初始化并认为它是反模式 .

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

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

那个's for sure the way to go. If you'坚持早期版本,看看Google Collections' ImmutableSet .

15 回答

  • 34

    我第二个Nat的答案,除了我会使用循环而不是创建并立即从asList(元素)抛出隐式List:

    static public Set<T> setOf(T ... elements) {
        Set set=new HashSet<T>(elements.size());
        for(T elm: elements) { set.add(elm); }
        return set;
        }
    
  • 2

    leak prone

    我决定加入进来 . 性能影响包括:磁盘操作解压缩(用于jar),类验证,perm-gen空间(用于Sun的Hotspot JVM) . 然而,最糟糕的是:它容易发生泄漏 . 你不能简单地回来 .

    Set<String> getFlavors(){
      return Collections.unmodifiableSet(flavors)
    }
    

    因此,如果集合转义到由不同类加载器加载的任何其他部分并且引用保留在那里,那么类classloader的整个树将被泄露 . 为避免这种情况,需要复制到HashMap, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}) . 不再那么可爱了 . 我不使用这个成语,我自己,而不是像 new LinkedHashSet(Arrays.asList("xxx","YYY"));

  • 3
    • 这将为每个成员调用 add() . 如果您可以找到更有效的方法将项目放入哈希集,那么使用它 . 请注意,如果您对此敏感,内部类可能会生成垃圾 .

    • 在我看来,好像上下文是 new 返回的对象,即 HashSet .

    • 如果你需要问...更有可能:跟你们相关的人会不会知道?理解和解释容易吗?如果您对两者都回答“是”,请随意使用它 .

  • 551

    除了效率之外,我很少发现自己希望在单元测试之外创建声明性集合 . 我确实认为双括号语法非常易读 .

    另一种实现列表声明性构造的方法是使用 Arrays.asList(T ...) ,如下所示:

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

    此方法的局限性当然是您无法控制要生成的特定类型的列表 .

  • 4

    到目前为止尚未指出此方法的一个属性是因为您创建内部类,所以在其范围内捕获整个包含类 . 这意味着只要你的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){
    
                }
    
            };
        }
    
  • 94

    Every time someone uses double brace initialisation, a kitten gets killed.

    除了语法相当不寻常而且不是真正的惯用语(当然味道有争议)之外,您在应用程序中不必要地创建了两个重要问题,which I've just recently blogged about in more detail here .

    1.你正在创建太多的匿名类

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

    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次...所有堆内存只是为了一点“语法糖”?

    2.您可能会造成内存泄漏!

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

    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 的封闭实例的引用 . 你可能不想冒这个风险:

    Memory Leak Right Here

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

    3.您可以假装Java具有 Map 文字

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

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

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

  • 19

    我正在研究这个问题,并决定进行比有效答案提供的更深入的测试 .

    这是代码:https://gist.github.com/4368924

    这是我的结论

    我很惊讶地发现,在大多数运行测试中,内部启动实际上更快(在某些情况下几乎翻倍) . 当大量工作时,好处似乎逐渐消失 . 有趣的是,在循环中创建3个对象的情况失去了它的好处,比其他情况更快 . 我不确定为什么会这样,应该做更多的测试来得出任何结论 . 创建具体实现可能有助于避免重新加载类定义(如果这是正在发生的事情)但是,很明显,在大多数情况下,对于单个项目构建,即使数量很大,也没有太多开销 . 一个挫折将是每个双支撑启动创建一个新的类文件,它将整个磁盘块添加到我们的应用程序的大小(或压缩时大约1k) . 占地面积小,但如果在很多地方使用它可能会产生影响 . 使用此1000次,您可能会在应用程序中添加整个MiB,这可能与嵌入式环境有关 . 我的结论?只要它没有被滥用,它就可以使用 .

    让我知道你的想法 :)

  • 3

    通常没什么特别低效的 . 你已经创建了一个子类并为它添加了一个构造函数对JVM来说通常并不重要 - 这是一种在面向对象语言中常常做的正常事情 . 我可以想到一些非常人为的例子,你可能会因此而导致效率低下(例如,你有一个反复调用的方法,由于这个子类而最终会混合使用不同的类,而传入的类通常是完全可预测的 - - 在后一种情况下,JIT编译器可以进行首先不可行的优化 . 但实际上,我认为重要的案例是非常人为的 .

    从你是否想要用大量匿名类“混乱”的角度来看,我会更多地看到这个问题 . 作为一个粗略的指南,请考虑使用成语,而不是使用事件处理程序的匿名类 .

    在(2)中,您位于对象的构造函数中,因此“this”指的是您正在构造的对象 . 这与任何其他构造函数没有什么不同 .

    至于(3),这实际上取决于谁提前知道这一点,那么我建议使用的基准是"do you see this in the source code to the JDK?"(在这种情况下,我不是匿名类的唯一内容) . 在大多数中等规模的项目中,我真的需要你的程序员在某些时候理解JDK源代码,所以在那里使用的语法或习语是"fair game" . 除此之外,我维护代码,否则评论或避免 .

  • 7

    双支撑初始化是一种不必要的破解,可能会引入内存泄漏和其他问题

    使用这个"trick"是没有正当理由的 . Guava提供了很好的immutable collections,其中包括静态工厂和构建器,允许您在清晰,可读和安全的地方填充您的集合 . 句法 .

    问题中的示例变为:

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

    这不仅更短,更容易阅读,而且避免了other answers中描述的双支撑模式的众多问题 . 当然,它的表现与直接构造的 HashMap 类似,但它很危险且容易出错,并且有更好的选择 .

    每当你发现自己考虑双括号初始化时,你应该重新检查你的API或introduce new ones以正确解决问题,而不是利用语法技巧 .

    Error-Prone现在flags this anti-pattern .

  • 2

    要创建集合,您可以使用varargs工厂方法而不是双支撑初始化:

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

    Google Collections库有许多这样的便捷方法,以及许多其他有用的功能 .

    至于成语的默默无闻,我遇到它并一直在 生产环境 代码中使用它 . 我更关心那些被允许编写 生产环境 代码的习语感到困惑的程序员 .

  • 39

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

    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虚拟机在使用它们时需要读取所有这些类,这可能会导致bytecode verfication进程中的某些时间等 . 更不用说为了存储所有那些 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!");
        }
    };
    

    所以它基本上是instance initialization block,它是anonymous inner class的一部分 .


    Joshua Bloch的Collection Literals proposalProject Coin是沿着以下方向:

    List<Integer> intList = [1, 2, 3, 4];
    
    Set<String> strSet = {"Apple", "Banana", "Cactus"};
    
    Map<String, Integer> truthMap = { "answer" : 42 };
    

    可悲的是,它既不是Java 7也不是8,并且被无限期搁置 .


    Experiment

    这里's the simple experiment I'已经测试过 - 使用两种方法,通过 add 方法将 "Hello""World!" 元素添加到1000 ArrayList 中:

    方法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 s和扩展 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
    

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

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

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

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

  • 35

    Mario Gleichman describes如何使用Java 1.5泛型函数来模拟Scala List文字,尽管遗憾的是你最终使用了不可变列表 .

    他定义了这个类:

    package literal;
    
    public class collection {
        public static <T> List<T> List(T...elems){
            return Arrays.asList( elems );
        }
    }
    

    并因此使用它:

    import static literal.collection.List;
    import static system.io.*;
    
    public class CollectionDemo {
        public void demoList(){
            List<String> slist = List( "a", "b", "c" );
            List<Integer> iList = List( 1, 2, 3 );
            for( String elem : List( "a", "java", "list" ) )
                System.out.println( elem );
        }
    }
    

    Google Collections现在是Guava的一部分,它支持列表构建的类似想法 . 在this interview,Jared Levy说:

    [...]最常用的功能,几乎出现在我编写的每个Java类中,都是静态方法,可以减少Java代码中重复键击的次数 . 能够输入如下命令非常方便:Map <OneClassWithALongName,AnotherClassWithALongName> = Maps.newHashMap(); List <String> animals = Lists.immutableList(“cat”,“dog”,“horse”);

    2014年7月10日:如果它只是像Python那样简单:

    animals = ['cat','dog','horse']

  • 9

    参加以下测试课程:

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

    这不是't look terribly inefficient to me. If I were worried about performance for something like this, I' d . 并且您的问题#2由上面的代码回答:您在内部类的隐式构造函数(和实例初始化程序)中,因此“ this ”指的是此内部类 .

    是的,这种语法很模糊,但注释可以澄清模糊的语法用法 . 为了澄清语法,大多数人都熟悉静态初始化程序块(JLS 8.7静态初始化程序):

    public class Sample1 {
        private static final String someVar;
        static {
            String temp = null;
            ..... // block of code setting temp
            someVar = temp;
        }
    }
    

    您也可以使用类似的语法(没有单词“ static ”)来构造函数使用(JLS 8.6实例初始化程序),尽管我从未在 生产环境 代码中看到过这种情况 . 这种情况不太为人所知 .

    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文档中的详细信息 .

  • 3

    虽然这种语法很方便,但它也会添加很多这个$ 0引用,因为这些引用会嵌套,除非在每个引用上设置断点,否则很难将步骤调试到初始化器中 . 出于这个原因,我只建议将它用于平庸的setter,特别是设置为常量,以及匿名子类无关紧要的地方(比如没有涉及序列化) .

  • 16

    加载许多类可以在开始时添加几毫秒 . 如果启动不那么重要,而且你在启动后看看类的效率,则没有区别 .

    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
    

相关问题