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

908
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 集合字面量来实现相同的效果。新的 JVM 语言(如 Scala、JRuby 和 Groovy)也提供了简洁的列表构造符号,并与 Java 很好地互操作。考虑到 DBI 混乱了类路径,稍微减慢了类加载速度,并使代码有点更加晦涩,我可能会避开它。但是,我计划向一个刚刚获得 SCJP 认证并热爱有关 Java 语义的友人展示这个技巧!;-) 谢谢大家!

2017年7月:Baeldung 有一个很好的总结 关于双括号初始化,并认为它是一种反模式。

2017年12月:@Basil Bourque 指出,在新的 Java 9 中,你可以这样说:

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

毫无疑问,这是正确的方法。如果你被困在早期版本中,请看一下Google Collections' ImmutableSet


41
我在这里看到的代码异味是,天真的读者会期望flavors是一个HashSet,但实际上它是一个匿名子类。 - Elazar Leibovich
6
如果您考虑的是运行而不是加载性能,那么没有区别,请查看我的回答。 - Peter Lawrey
5
我很喜欢你创建了摘要,我认为这对你提高理解力和社区都是有益的练习。 - Patrick Murphy
3
在我看来,这并不难懂。读者应该知道双括号初始化的含义......哦,等等,@ElazarLeibovich 已经在 他的评论 中说过了。双括号初始化本身并不存在于语言构造中,它只是匿名子类和实例初始化器的组合。唯一需要注意的是,人们需要意识到这一点。 - MC Emperor
12
Java 9提供了“不可变集合静态工厂方法”,在某些情况下可以替代DCI。示例代码:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ; - Basil Bourque
显示剩余8条评论
15个回答

3
Mario Gleichman 描述 如何使用Java 1.5泛型函数来模拟Scala列表字面量,但遗憾的是你最终得到的是 不可变 列表。
他定义了这个类:
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']。2020年2月21日:在Java 11中,您现在可以这样说:animals = List.of(“cat”,“dog”,“horse”)。

3

我赞同Nat的回答,不过我会使用循环而不是创建并立即丢弃asList(elements)隐式列表:

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

1
为什么?新对象将在Eden空间中创建,因此只需要两到三个指针添加即可实例化。JVM可能会注意到它从未超出方法范围,因此将其分配到堆栈上。 - Nat
是的,最终代码很可能比那段代码更有效率(尽管您可以通过告诉“HashSet”建议的容量 - 记住负载因子来改进它)。 - Tom Hawtin - tackline
好的,HashSet构造函数必须进行迭代,因此它不会变得高效。为重复使用创建的库代码应始终努力成为最好的可能性。 - Lawrence Dol

3
我进行了一些研究,并决定进行比有效答案提供的更深入的测试。以下是代码:https://gist.github.com/4368924,这是我的结论。
令人惊讶的是,在大多数运行测试中,内部初始化实际上更快(在某些情况下几乎是两倍)。当使用大数字时,该效益似乎逐渐消失。
有趣的是,在循环中创建3个对象的情况比其他情况更快地失去了优势。我不确定为什么会发生这种情况,需要进行更多的测试才能得出结论。创建具体实现可能有助于避免重新加载类定义(如果发生了这种情况)。
然而,很明显,在大多数情况下单项建设并没有观察到太多开销,即使是在大型数字下也是如此。
一个缺点是每个双括号初始化都会创建一个新的类文件,将一个整个磁盘块添加到应用程序的大小中(或者在压缩时约1K)。一个小的足迹,但是如果在多个位置使用它,它可能会对性能产生影响。如果使用1000次,则可能向您的应用程序添加整个MiB,这可能会在嵌入式环境中引起关注。
我的结论?只要不滥用,就可以使用它。

5
这不是一个有效的测试。该代码创建对象但没有使用它们,这使得优化器可以省略整个实例创建过程。唯一剩下的副作用是推进随机数序列,而这个过程的开销已经超过了这些测试中的任何其他内容。 - Holger

3

虽然这种语法很方便,但是它也增加了很多this$0的引用,因为这些引用变得嵌套,除非在每个引用上设置断点,否则很难逐步调试初始化器。因此,我只建议将其用于平凡的setter,特别是设置为常量的setter,以及匿名子类不重要的地方(比如没有序列化涉及的地方)。


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

  2. 在我看来,上下文是由new返回的对象,即HashSet

  3. 如果你需要问... 更可能的是:接下来的人是否会知道这些或不知道?它是否容易理解和解释?如果两者都回答“是”,请随意使用它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接