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

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 Colltions和提议的Java7 Collection文字来达到同样的效果。较新的JVM语言,如Scala、JRuby和Groovy也为列表构造提供了简洁的符号,并且与Java很好地互操作。鉴于DBI会弄乱类路径,减缓类加载速度,并且使代码更加模糊,我可能会回避它。然而,我打算把这个告诉一个刚刚拿到SCJP并且喜欢关于Java语义学的善意争论的朋友!;-)谢谢大家!

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

2017年12月:@Basil Bourque指出,在新的Java9中,你可以说:

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

这肯定是要走的路。如果您坚持使用早期版本,请查看Google集合的ImMutableSet

145499 次浏览

使用以下测试类:

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静态初始化器):

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

您还可以使用类似的语法(没有单词“static”)来使用构造函数(JLS 8.6实例初始化器),尽管我从未见过在生产代码中使用这种语法。这是鲜为人知的。

public class Sample2 {private final String someVar;
// This is an instance initializer{String temp = null;..... // block of code setting tempsomeVar = 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留档中的细节。

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

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

  3. 如果你需要问……更有可能的是:追随你的人知道吗?这是否容易理解和解释?如果你能对两者都回答“是”,请随意使用它。

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

实现列表声明式构造的另一种方法是像这样使用Arrays.asList(T ...)

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

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

这是当我对匿名内部类过于得意忘形时的问题:

2009/05/27  16:35             1,602 DemoApp2$1.class2009/05/27  16:35             1,976 DemoApp2$10.class2009/05/27  16:35             1,919 DemoApp2$11.class2009/05/27  16:35             2,404 DemoApp2$12.class2009/05/27  16:35             1,197 DemoApp2$13.class
/* snip */
2009/05/27  16:35             1,953 DemoApp2$30.class2009/05/27  16:35             1,910 DemoApp2$31.class2009/05/27  16:35             2,007 DemoApp2$32.class2009/05/27  16:35               926 DemoApp2$33$1$1.class2009/05/27  16:35             4,104 DemoApp2$33$1.class2009/05/27  16:35             2,849 DemoApp2$33.class2009/05/27  16:35               926 DemoApp2$34$1$1.class2009/05/27  16:35             4,234 DemoApp2$34$1.class2009/05/27  16:35             2,849 DemoApp2$34.class
/* snip */
2009/05/27  16:35               614 DemoApp2$40.class2009/05/27  16:35             2,344 DemoApp2$5.class2009/05/27  16:35             1,551 DemoApp2$6.class2009/05/27  16:35             1,604 DemoApp2$7.class2009/05/27  16:35             1,809 DemoApp2$8.class2009/05/27  16:35             2,022 DemoApp2$9.class

这些都是我在制作一个简单应用程序时生成的类,并使用了大量的匿名内部类——每个类都将编译成一个单独的class文件。

如前所述,“双括号初始化”是一个带有实例初始化块的匿名内部类,这意味着为每个“初始化”创建一个新类,所有这些通常都是为了制作单个对象。

考虑到Java虚拟机在使用它们时需要读取所有这些类,这可能会导致字节码验证进程中的一些时间等。更不用说为了存储所有这些class文件而增加所需的磁盘空间了。

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


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

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

可悲的是,它没有成功既不是Java7也不是8,并且被无限期搁置。


实验

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

方法一:双括号初始化

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

请注意,初始化1000ArrayList和扩展ArrayList的1000个匿名内部类所花费的时间是使用System.currentTimeMillis检查的,所以计时器的分辨率不是很高。在我的Windows系统上,分辨率大约是15-16毫秒。

两次测试中的10次测试结果如下:

Test1 Times (ms)           Test2 Times (ms)----------------           ----------------187                          0203                          0203                          0188                          0188                          0187                          0203                          0188                          0188                          0203                          0

可以看出,双括号初始化的执行时间明显在190 ms左右。

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

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

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

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

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

Google集合库有很多像这样的方便方法,以及大量其他有用的功能。

至于成语的晦涩难懂,我经常遇到它并在生产代码中使用它。我更关心那些被允许编写生产代码的成语弄糊涂的程序员。

我支持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;}

它通常没有什么特别低效的地方。对于JVM来说,你已经创建了一个子类并向其添加了一个构造函数通常并不重要——这在面向对象语言中是正常的、日常的事情。我可以想到一些相当人为的情况,你这样做可能会导致效率低下(例如,你有一个重复调用的方法,由于这个子类,它最终采用了不同类的混合物,而传入的普通类将是完全可预测的——在后一种情况下,即时编译器可以进行第一种不可行的优化)。但实际上,我认为这很重要的情况是非常人为的。

我更倾向于从是否要使用大量匿名类“混乱”的角度来看待这个问题。作为一个粗略的指南,请考虑使用该习语,而不是使用事件处理程序的匿名类。

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

至于(3),我想这真的取决于谁在维护你的代码。如果你事先不知道这一点,那么我建议使用的一个基准是“你在JDK的源代码中看到了这一点吗?”(在这种情况下,我不记得看到很多匿名初始化程序,当然也不是在匿名类的只有内容的情况下)。在大多数中等规模的项目中,我认为你真的需要你的程序员在某个时候理解JDK源代码,所以那里使用的任何语法或习语都是“公平的游戏”。除此之外,我想说的是,如果您可以控制谁在维护代码,请对人们进行语法培训,否则请评论或避免。

到目前为止还没有被指出的这种方法的一个属性是,因为你创建了内部类,所以整个包含类都被捕获在它的作用域中。这意味着只要你的Set还活着,它就会保留一个指向包含实例(this$0)的指针,并防止它被垃圾收集,这可能是一个问题。

这一点,以及一个新类首先被创建的事实,即使一个常规的HashSet可以很好地工作(甚至更好),让我不想使用这个构造(即使我真的很渴望语法糖)。

第二个问题:新的HashSet必须是实例初始化器中使用的“this”……有人能阐明这种机制吗?我天真地期望“this”指的是初始化“味道”的对象。

这就是内部类的工作方式。它们有自己的this,但它们也有指向父实例的指针,这样你也可以调用包含对象的方法。在命名冲突的情况下,内部类(在你的情况下是HashSet)优先,但你可以在“this”前面加上类名来获取外部方法。

public class Test {
public void add(Object o) {}
public Set<String> makeSet() {return new HashSet<String>() \{\{add("hello"); // HashSetTest.this.add("hello"); // outer instance}};}}

要清楚正在创建的匿名子类,您也可以在其中定义方法。例如覆盖HashSet.add()

    public Set<String> makeSet() {return new HashSet<String>() \{\{add("hello"); // not HashSet anymore ...}
@Overrideboolean add(String s){
}
};}

Mario Gleichman描述如何使用Java1.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 );}}

谷歌集合,现在是番石榴的一部分,支持类似的列表构建想法。在这次采访中,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日:在Java11你现在可以说:

animals = List.of(“cat”, “dog”, “horse”)

容易泄漏

我决定加入进来。对性能的影响包括:磁盘操作+解压缩(对于jar)、类验证、永久生成空间(对于Sun的Hotspot JVM)。然而,最糟糕的是:它容易泄漏。你不能简单地返回。

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

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

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

代码如下:https://gist.github.com/4368924

这就是我的结论

我惊讶地发现,在大多数运行测试中,内部启动实际上更快(在某些情况下几乎是两倍)。当处理大量数据时,好处似乎消失了。

有趣的是,在循环上创建3个对象的情况会比其他情况更快地失去它的好处。我不知道为什么会发生这种情况,应该做更多的测试来得出任何结论。创建具体的实现可能有助于避免重新加载类定义(如果发生了这种情况)

然而,很明显,在大多数情况下,它观察到的单个项目建设的开销并不大,即使数量很大。

一个缺点是,每个双括号启动都会创建一个新的类文件,该文件将整个磁盘块添加到我们的应用程序的大小(或压缩时大约1k)。占用空间很小,但如果它在许多地方使用,它可能会产生影响。使用这个1000次,你可能会为你的应用程序添加一个完整的MiB,这在嵌入式环境中可能会令人担忧。

我的结论?只要不滥用都可以用

让我知道你的想法:)

加载许多类可以在启动时增加一些毫秒。如果启动不是那么关键,并且您在启动后查看类的效率,则没有区别。

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 nsdouble braced collections took 34 ns and plain collections took 36 nsdouble braced collections took 36 ns and plain collections took 36 nsdouble braced collections took 36 ns and plain collections took 36 nsdouble braced collections took 36 ns and plain collections took 36 nsdouble braced collections took 36 ns and plain collections took 36 nsdouble braced collections took 36 ns and plain collections took 36 nsdouble braced collections took 36 ns and plain collections took 36 nsdouble braced collections took 36 ns and plain collections took 36 ns

虽然这种语法很方便,但它也增加了很多this0美元的引用,因为这些引用变得嵌套,并且除非在每个初始化器上设置断点,否则很难单步调试到初始化器中。出于这个原因,我只建议将其用于平庸的setter,尤其是设置为常量,以及匿名子类无关紧要的地方(比如不涉及序列化)。

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

除了语法相当不寻常并且不是真正的惯用(当然,品味是有争议的)之外,您在应用程序中不必要地创建了两个重要问题,我最近在博客上更详细地介绍了这一点

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.classTest$1$1$2.classTest$1$1.classTest$1.classTest.class

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

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

如果您采用上述代码并从方法返回该映射,则该方法的调用者可能会毫无戒心地持有无法垃圾收集的非常重的资源。考虑以下示例:

public class ReallyHeavyObject {
// Just to illustrate...private int[] tonsOfValues;private Resource[] tonsOfResources;
// This method almost does nothingpublic 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的封闭实例的引用。您可能不想冒这样的风险:

此处内存泄漏

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

3.你可以假装Java有地图文字

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

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

有些人可能会发现这在句法上很刺激。

双括号初始化是一种不必要的黑客行为,可能会引入内存泄漏和其他问题

使用这个“技巧”没有正当的理由。Guava提供了很好的不可变集合,其中包括静态工厂和构建器,允许您在以干净、可读和安全语法声明的地方填充集合。

问题中的例子变成了:

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

这不仅更短,更易于阅读,而且避免了其他答案中描述的双括号模式的许多问题。当然,它的性能类似于直接构造的HashMap,但它很危险且容易出错,还有更好的选择。

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

易出错现在标记这个反模式