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个回答

656

当我过于沉迷于匿名内部类时,会遇到一个问题:

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文件所需的磁盘空间。
使用双括号初始化似乎会带来一些开销,因此过度使用可能不是一个好主意。但正如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!");
    }
};

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


Joshua Bloch提出的集合字面值建议属于Project Coin,大致如下:

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:双括号初始化

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

方法2:实例化一个ArrayList并使用add方法添加元素

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

请注意,初始化1000个ArrayList和1000个扩展ArrayList的匿名内部类的经过时间是使用System.currentTimeMillis检查的,因此计时器的分辨率不是很高。在我的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毫秒。当然,应该考虑计时器的分辨率,但很可能是在15毫秒以下。

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

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


11
“Probably”是关键词。除非进行测量,否则关于性能的陈述毫无意义。 - Instance Hunter
17
你做得非常出色,我几乎不想这么说,但是Test1的时间可能会受到课程负载的影响。有人运行每个测试的单个实例,比如在一个for循环中运行1,000次,然后再在第二个for循环中运行1,000或10,000次,并打印出时间差异(System.nanoTime())。第一个for循环应该可以通过所有预热效应(JIT,类加载等)。但两个测试模拟了不同的用例。我将尝试在明天的工作中运行此操作。 - Jim Ferrans
13
@Jim Ferrans:我相当确信Test1的时间是由于类加载造成的。但是,使用双括号初始化的后果是必须处理来自类加载的问题。我认为,双括号初始化的大多数用例都是用于一次性初始化,该测试更接近于此类初始化的典型用例条件。 我相信对每个测试进行多次迭代会使执行时间差距变小。 - coobird
78
这证明了a) 双大括号初始化速度较慢,b) 即使您执行它1000次,您也可能不会注意到差异。而且这不可能是内部循环中的瓶颈。它最多只会对执行一次带来微小的惩罚。 - Michael Myers
20
如果使用DBI可以使代码更易读或表达,那就使用它。它会增加JVM执行的工作量这一事实本身不是反对它的有效论据。如果是这样的话,那么我们也应该担心额外的辅助方法/类,而更喜欢具有较少方法的巨大类... - Rogério
显示剩余21条评论

115

这种方法的一个特点是,因为你创建了内部类,整个包含类都被包含在它的范围内。这意味着只要您的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){

            }

        };
    }

5
关于隐藏对包含类的引用的观点非常好。在原始示例中,实例初始化程序调用的是新HashSet<String>的add()方法,而不是Test.this.add()。这让我想到可能还有其他事情发生了。是否存在一个匿名内部类来表示HashSet<String>,就像Nathan Kitchen所建议的那样? - Jim Ferrans
1
如果数据结构需要进行序列化,则对包含类的引用也可能很危险。包含类也将被序列化,因此必须可序列化。这可能会导致难以理解的错误。 - Just another Java programmer
1
而且不仅仅是 this$0。当这个技巧用于非常量值时,访问的变量的值也会被捕获,并且即使从集合中删除相应的元素,这些值也会保持引用。在最极端的情况下,你可能会得到一个空集合,其中有数百个对它初始化的对象的引用。 - Holger

73
每次有人使用双括号初始化,就会有一只小猫被杀害。
除了语法相当不寻常且不是真正的惯用语(当然,品味是有争议的),你在应用程序中不必要地创建了两个重大问题我刚刚在这里更详细地博客介绍过
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"); }};

一些人可能会觉得这在语法上很刺激。

12
拯救小猫!好的回答! - Aris2World

41

接下来是一个测试类:

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

在我看来,这并不显得很低效。如果我对这样的东西担心性能,我会对其进行剖析。至于你的第二个问题,上面的代码已经回答了它:你在内部类的隐式构造函数(和实例初始化器)中,所以"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;
    }
}

您还可以使用类似语法(不带“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。这可能会带来一些小的开销(很可能可以忽略不计)。但是,在担心它之前,最好进行性能分析。

对于您的第二个问题,上述代码是双层括号初始化的逻辑和明确的等效形式,并且可以清楚地看出 "this" 指的是哪个内部类——即扩展了 HashSet 的内部类。

如果您对实例初始化程序的细节有疑问,请参阅JLS文档中的详细信息。


Eddie,非常好的解释。如果JVM字节码像反编译一样干净,执行速度就足够快了,尽管我有点担心额外的.class文件会使事情变得混乱。我仍然很好奇为什么实例初始化器的构造函数将“this”视为新的HashSet<String>实例而不是Test实例。这只是最新的Java语言规范中明确指定的行为,以支持这种习惯用法吗? - Jim Ferrans
我更新了我的回答。我遗漏了Test类的样板,这导致了混淆。我将其放入我的答案中以使事情更加明显。我还提到了在此习语中使用的实例初始化程序块的JLS部分。 - Eddie
1
@Jim,“this”的解释并不是一个特例;它只是指向最内层封闭类的实例,也就是HashSet<String>的匿名子类的实例。 - Nathan Kitchen
很抱歉四年半后才插话。但反编译的类文件(您的第二个代码块)的好处是它不是有效的Java!它将super()作为隐式构造函数的第二行,但它必须放在第一行。(我已经测试过了,它无法编译。) - chiastic-security
1
有时候反编译器生成的代码无法编译。 - Eddie
如果我们忽略性能方面,Set<String> flavors = new HashSet<String>(); Collections.addAll(flavors, "vanilla", "strawberry", "chocolate", "butter pecan"); 仍然更短,更易读,也适用于不支持子类化的集合类型,并且没有双花括号带来的缺点。 - Holger

37

容易出现内存泄漏

我决定发表一下自己的看法。性能影响包括:磁盘操作+解压缩(对于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"));


3
幸运的是,从Java 8开始,PermGen不再存在了。我想仍然会有影响,但不会导致非常晦涩的错误消息。 - Joey
2
@Joey,无论内存是否由GC(perm gen)直接管理,都没有任何影响。在metaspace中的泄漏仍然是泄漏,除非限制meta,否则像oom_killer在Linux中的东西不会导致OOM(out of perm gen)。 - bestsss

19

如果启动时间不是非常关键,且您在启动后查看类的效率,则加载许多类可能会增加一些毫秒,但并不会有任何区别。

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

2
除非设置一些晦涩的JVM选项以允许类卸载和PermGen空间的垃圾收集,否则使用DBI过度将会使您的PermGen空间消失。考虑到Java作为服务器端语言的普及性,内存/PermGen问题至少应该被提及。 - aroth
1
@aroth 这是一个很好的观点。我承认在16年的Java开发工作中,我从未遇到过需要调整PermGen(或Metaspace)的系统。对于我所涉及的系统,代码大小始终保持得相当小。 - Peter Lawrey
3
“compareCollections”函数中的条件应该使用“||” 连接而不是 “&&” 。使用 “&&” 不仅在语义上不正确,而且会抵消度量性能的意图,因为只有第一个条件会被测试。此外,聪明的优化器可以识别出在迭代期间这些条件永远不会改变。 - Holger
@aroth,更新一下:自从Java 8以来,虚拟机不再使用任何永久代了。 - Angel O'Sphere
@AngelO'Sphere,permgen已经消失了,但Metaspace是它的继承者(具有一些不同的行为/限制),但类定义仍然存在于内存中的某个地方--这并不是免费的。 - William Price

16

你可以使用变长参数的工厂方法来创建集合,而不是使用双括号初始化:

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

Google Collections库有很多方便的方法,以及许多其他有用的功能。
至于该习语的晦涩性,我经常在生产代码中遇到它并使用它。我更担心混淆这个习语的程序员被允许编写生产代码。

哈哈!;-) 我实际上是一个 Rip van Winkle,从 1.2 版本的 Java 回归(我曾在 Java 中编写了 VoiceXML 语音 Web 浏览器,网址为 http://evolution.voxeo.com/)。学习泛型、参数化类型、集合、java.util.concurrent、新的 for 循环语法等非常有趣。现在它是一种更好的语言。就您所说的,即使 DBI 背后的机制一开始似乎很难理解,但代码的含义应该是非常清晰的。 - Jim Ferrans

11

除了效率之外,我很少希望在单元测试之外使用声明性集合创建。 我确信双括号语法非常易读。

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

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

这种方法的限制是你无法控制生成的具体列表类型。

1
Arrays.asList() 是我通常使用的,但你说得对,这种情况主要出现在单元测试中;真正的代码会从数据库查询、XML等构建列表。 - Jim Ferrans
8
注意asList方法:返回的列表不支持添加或删除元素。每当我使用asList时,我会将结果列表传递给构造函数,例如new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))来解决这个问题。 - Michael Myers

7

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

没有正当理由使用这个“技巧”。Guava提供了漂亮的不可变集合,包括静态工厂和构建器,允许您在声明集合时以清晰、可读和安全的语法填充集合。

问题中的示例变为:

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

这种写法不仅更短、更易读,而且避免了双括号模式(其他答案中描述的诸多问题)。它虽然执行效果与直接构造的 HashMap 相似,但却很危险、容易出错,而且有更好的选择。
每当你考虑使用双括号初始化时,都应该重新审视你的 API 或 引入新的 API 来正确解决问题,而不是利用语法技巧。 Error-Prone 现在标记此反模式

3
尽管这个回答有一些合理的观点,但归根结底可以概括为“如何避免生成不必要的匿名类?使用一个框架,其中包含更多的类!” - Agent_L
1
我认为这归结于“使用正确的工具来完成工作,而不是可能会使应用程序崩溃的hack”。Guava是一个非常常见的库,应用程序可以包含它(如果您不使用它,您肯定会错过一些好东西),但即使您不想使用它,您也可以并且应该避免双括号初始化。 - dimo414
1
双括号初始化会如何导致内存泄漏? - Angel O'Sphere
@AngelO'Sphere DBI是创建内部类的一种混淆方式,因此保留对其封闭类的隐式引用(除非仅在“静态”上下文中使用)。我问题底部的Error-Prone链接进一步讨论了这个问题。 - dimo414
我认为这是一个品味问题。而且它并没有什么真正难懂的地方。 - Angel O'Sphere
我不认为冒着内存泄漏的风险是一种品味问题,但我并不想争论这个观点。如果你真的觉得这是你可用的最佳选项,那么请非常小心地使用它。 - dimo414

6
通常情况下,这种做法并不会特别低效。对于JVM来说,你创建一个子类并添加构造函数并不会有什么影响——这在面向对象语言中是很正常的日常操作。我可以想到一些非常牵强附会的情况,在这些情况下,由于创建了子类,某个方法会反复调用并且传递了多种不同的类,而如果传递普通的类,则完全可以预测——在后者的情况下,JIT编译器可以进行优化,而在前者则不可行。但实际上,我认为这种情况非常牵强附会。
我认为应该从是否要“用大量匿名类来混淆事情”的角度考虑问题。作为一个粗略的指南,考虑使用这种习惯用法的频率,就像使用匿名类处理事件处理程序一样。
在(2)中,你在对象的构造函数内部,所以“this”指的是你正在构造的对象。这与任何其他构造函数没有区别。
至于(3),这实际上取决于谁来维护你的代码。如果你事先不知道这一点,那么我建议使用一个基准测试:“你在JDK的源代码中看到这个了吗?”(在这种情况下,我不记得看到过很多匿名的初始化器,当然也没有在只有匿名类的情况下看到过)。在大多数中等规模的项目中,我认为你确实需要让程序员在某个时候理解JDK源代码,因此任何在那里使用的语法或习惯用法都是“公平竞争”的。除此之外,如果你控制着维护代码的人,那么我会说,对这种语法进行培训,否则就注释或避免使用。

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