关于性能:Java”双括号初始化”的效率?

Efficiency of Java “Double Brace Initialization”?

在Java的隐藏特性中,顶部答案提到双括号初始化,具有非常诱人的语法:

1
2
3
4
5
6
Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

这个习惯用法创建了一个匿名的内部类,其中只包含一个实例初始值设定项,"可以在包含范围内使用任何[…]方法"。

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

第二个问题:新的哈希集必须是实例初始值设定项中使用的"this"…有人能解释一下这个机制吗?

第三个问题:这个习语太模糊了,不能用在生产代码中吗?

非常非常好的回答,谢谢大家。关于问题(3),人们认为语法应该是清晰的(尽管我建议偶尔发表评论,特别是如果您的代码将传递给不熟悉它的开发人员)。

对于问题(1),生成的代码应该运行得很快。额外的.class文件确实会导致jar文件混乱,并且稍微减慢程序的启动速度(这要归功于@coobird的测量)。@Thilo指出垃圾收集会受到影响,在某些情况下,额外加载类的内存成本可能是一个因素。

问题(2)对我来说是最有趣的。如果我理解答案,那么DBI中发生的事情是匿名内部类扩展了由新操作符构造的对象的类,因此具有引用正在构造的实例的"this"值。非常整洁。

总的来说,DBI给我的印象是一种智力上的好奇。Coobird和其他人指出,使用Arrays.asList、ValARGS方法、谷歌集合和建议的Java 7集合文字可以实现相同的效果。较新的JVM语言如斯卡拉、JRuBy和Groovy也为列表构造提供简明的注释,并与Java进行良好的互操作。考虑到DBI会使类路径变得混乱,使类加载速度变慢,使代码变得更加模糊,我可能会避开它。不过,我打算把这个故事介绍给一个刚刚得到他的SCJP的朋友,他喜欢关于Java语义的好脾气的JavaS!;-)谢谢大家!

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

12/2017:Basil Bourque注意到,在新的Java 9中,你可以说:

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

这是一条必经之路。如果你坚持使用早期的版本,看看谷歌收藏的不变集。


当我过于沉迷于匿名的内部类时,问题是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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文件而增加了所需的磁盘空间。

在使用双括号初始化时,似乎有一些开销,所以使用它可能不是一个好主意。但正如埃迪在评论中所指出的,完全不可能确定其影响。

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

1
2
3
4
List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

它看起来像是一个"隐藏"的Java特性,但它只是一个重写:

1
2
3
4
5
6
7
8
List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

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

Joshua Bloch对硬币项目的文字收集建议遵循以下原则:

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

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

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

遗憾的是,它没有进入Java 7和8中,并被无限期搁置。

实验

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

方法1:双括号初始化

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

方法2:实例化ArrayListadd

1
2
3
List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

我创建了一个简单的程序来编写一个Java源文件,使用两种方法来执行1000个初始化:

试验1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
  }
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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毫秒,当然要考虑到定时器的分辨率,但可能低于15毫秒。

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

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


这种方法的一个特性到目前为止还没有被指出,因为您创建了内部类,所以整个包含类都被捕获在其范围内。这意味着只要您的集合是活动的,它将保留指向包含实例(this$0的指针,并防止该实例被垃圾收集,这可能是一个问题。

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

Second question: The new HashSet must be the"this" used in the instance initializer ... can anyone shed light on the mechanism? I'd have naively expected"this" to refer to the object initializing"flavors".

这就是内部类的工作方式。它们得到自己的this,但它们也有指向父实例的指针,因此您也可以对包含对象调用方法。在命名冲突的情况下,内部类(在您的情况下是哈希集)优先,但您也可以在"this"前面加上类名以获取外部方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }


每次有人用双括号初始化,小猫就会被杀死。

除了语法很不寻常而且不太习惯(当然,味觉是有争议的),您在应用程序中不必要地创建了两个重要问题,我最近在博客中更详细地介绍了这两个问题。

1。你创建的匿名类太多了

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

1
2
3
4
5
6
7
8
9
10
11
12
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");
        }});
    }});
}};

…将生成这些类:

1
2
3
4
5
Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

对于您的类加载器来说,这是一个相当大的开销——毫无意义!当然,如果你只做一次,初始化时间不会太长。但是,如果您在整个企业应用程序中这样做了20000次…所有这些堆内存只是为了一点"语法糖"?

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

如果您使用上述代码并从一个方法返回该映射,那么该方法的调用方可能会毫无疑问地持有无法进行垃圾收集的大量资源。请考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

image from http://blog.jooq.org/2014/12/08/dont be smart the double curly brakes anti pattern/

三。你可以假装Java有地图文字。

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

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

有些人可能会发现这种句法上的刺激。


易泄漏的

我决定插话。性能影响包括:磁盘操作+unzip(对于jar)、类验证、perm-gen空间(对于Sun的Hotspot JVM)。然而,最糟糕的是:它很容易泄漏。你不能简单地回来。

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

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


参加以下测试课程:

1
2
3
4
5
6
7
8
9
10
public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}

然后反编译类文件,我看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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由上面的代码回答:您在内部类的隐式构造函数(和实例初始值设定项)中,所以"EDOCX1"〔0〕指的是这个内部类。

是的,这个语法是模糊的,但是注释可以澄清模糊的语法用法。为了澄清语法,大多数人都熟悉静态初始值设定项块(jls 8.7静态初始值设定项):

1
2
3
4
5
6
7
8
public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

您也可以使用类似的语法(不使用"EDOCX1"(1))来使用构造函数(jls 8.6实例初始值设定项),尽管我从未在生产代码中看到过这种用法。这是不常见的。

1
2
3
4
5
6
7
8
9
10
public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

如果没有默认的构造函数,那么编译器会将{}之间的代码块转换为构造函数。记住这一点,解开双括号代码:

1
2
3
4
5
6
7
8
9
10
public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}

编译器将最内部大括号之间的代码块转换为构造函数。最外面的大括号分隔匿名内部类。要将此作为使所有内容都非匿名的最后一步,请执行以下操作:

1
2
3
4
5
6
7
8
9
10
11
12
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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();
    }
}

印刷品

1
2
3
4
5
6
7
8
9
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


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

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

谷歌收藏库有很多这样的方便方法,还有很多其他有用的功能。

至于成语的晦涩,我经常遇到它,并在生产代码中使用它。我更关心那些被允许编写生产代码的习惯用法所迷惑的程序员。


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

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

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

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


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

我更愿意从你是否想用大量匿名类"混乱事情"的角度来看待这个问题。作为一个粗略的指南,考虑使用这个成语,不要超过匿名类来处理事件。

在(2)中,您在对象的构造函数中,所以"this"指的是您正在构造的对象。这与任何其他构造函数都没有区别。

至于(3),我想这真的取决于谁在维护您的代码。如果您事先不知道这一点,那么我建议使用的基准是"您在JDK的源代码中看到这一点了吗?"(在本例中,我不记得看到过许多匿名初始化器,当然在匿名类的唯一内容的情况下不会看到)。在大多数中等规模的项目中,我认为您确实需要您的程序员在某个时刻或其他时刻理解JDK源代码,所以这里使用的任何语法或习惯用法都是"公平游戏"。除此之外,我想说的是,如果您能够控制谁维护代码,就可以训练人们使用这种语法,否则就要注释或避免注释。


双括号初始化是一个不必要的黑客程序,它会导致内存泄漏和其他问题。

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

问题中的例子是:

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

这不仅简短易读,而且避免了其他答案中描述的双重支撑模式的众多问题。当然,它的性能与直接构造的HashMap类似,但它危险且容易出错,而且有更好的选择。

每当你发现自己在考虑双重支撑的初始化时,你应该重新检查你的API或者引入新的API来正确地解决这个问题,而不是利用句法技巧。

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


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

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

这是我的结论

I was surprised to find that in most of the run tests the internal initiation was actually faster (almost double in some cases). When working with large numbers the benefit seems to fade away.

Interestingly, the case that creates 3 objects on the loop loses it's benefit rans out sooner than on the other cases. I am not sure why this is happening and more testing should be done to reach any conclusions. Creating concrete implementations may help to avoid the class definition to be reloaded (if that's what's happening)

However, it is clear that not much overhead it observed in most cases for the single item building, even with large numbers.

One set back would be the fact that each of the double brace initiations creates a new class file that adds a whole disk block to the size of our application (or about 1k when compressed). A small footprint, but if it's used in many places it could potentially have an impact. Use this 1000 times and you are potentially adding a whole MiB to you applicaiton, which may be concerning on an embedded environment.

My conclusion? It can be ok to use as long as it is not abused.

让我知道你的想法:)


虽然这种语法很方便,但当这些引用成为嵌套时,它还添加了大量$0引用,并且除非在每个引用上设置了断点,否则很难将调试单步执行到初始值设定项中。出于这个原因,我只建议将其用于普通的setter,尤其是设置为常量,以及匿名子类不重要的地方(比如不涉及序列化)。


Mario Gleichman描述了如何使用Java 1.5泛型函数来模拟Scala列表文字,但遗憾的是,您可以使用不可变列表。

他定义了这个阶级:

1
2
3
4
5
6
7
package literal;

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

并使用它:

1
2
3
4
5
6
7
8
9
10
11
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的一部分,支持类似的列表构建思想。在这次采访中,贾里德·利维说:

[...] the most heavily-used features, which appear in almost every Java class I write, are static methods that reduce the number of repetitive keystrokes in your Java code. It's so convenient being able to enter commands like the following:

Map = Maps.newHashMap();

List animals = Lists.immutableList("cat","dog","horse");

2014年10月7日:如果它能像python一样简单的话:

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


我是Nat的第二个答案,除了我使用循环而不是创建并立即从a s list(元素)中抛出隐式列表之外:

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


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

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

  • 如果你需要问…更可能的情况是:那些追求你的人是否知道这一点?容易理解和解释吗?如果你能对两者都回答"是",请随意使用。