Java"双支撑初始化"的效率?

Jim*_*ans 786 java collections performance initialization

Java隐藏功能中,最佳答案提到了Double Brace Initialization,它具有非常诱人的语法:

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

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

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

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

第三个问题:在生产代码中使用这个成语是否过于模糊

简介:非常非常好的答案,谢谢大家.在问题(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 对双支撑初始化有很好的总结,并认为它是一种反模式.

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

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");
Run Code Online (Sandbox Code Playgroud)

这肯定是要走的路.如果您遇到早期版本,请查看Google Collections的ImmutableSet.

coo*_*ird 590

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

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


仅供参考,双支撑初始化如下:

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};
Run Code Online (Sandbox Code Playgroud)

它看起来像Java的"隐藏"功能,但它只是重写:

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};
Run Code Online (Sandbox Code Playgroud)

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


Joshua Bloch 关于Project CoinCollection Literals提案遵循以下方针:

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

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

Map<String, Integer> truthMap = { "answer" : 42 };
Run Code Online (Sandbox Code Playgroud)

可悲的是,它没有进入Java 7和8并且无限期搁置.


实验

这是我测试过的简单实验 - ArrayList用元素制作1000 秒"Hello""World!"通过add方法添加到它们中,使用以下两种方法:

方法1:双支撑初始化

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};
Run Code Online (Sandbox Code Playgroud)

方法2:实例化ArrayListadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");
Run Code Online (Sandbox Code Playgroud)

我创建了一个简单的程序来编写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);
  }
}
Run Code Online (Sandbox Code Playgroud)

测试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);
  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,使用the检查初始化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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 这证明了a)双支撑初始化较慢,b)即使你做了1000次,你可能也不会注意到差异.并不是说这可能是内循环的瓶颈.它在非常糟糕的情况下实施了一次微小的罚款. (67认同)
  • 你做得非常出色我几乎不想这么说,但是Test1次可能会受到课堂负荷的支配.有趣的是,有人在for循环中运行每个测试的单个实例,例如1,000次,然后在第二次循环中再次运行1,000或10,000次并打印出时间差(System.nanoTime()).第一个for循环应该超过所有预热效果(JIT,classload,例如).这两个测试都模拟了不同的用例.我明天会在工作中尝试这样做. (14认同)
  • 如果使用DBI使代码更具可读性或表现力,那么使用它.它增加了JVM必须执行的工作的事实本身并不是一个有效的参数.如果是,那么我们也应该担心额外的辅助方法/类,而不是使用更少方法的大类... (14认同)
  • "可能"是一个有效的词.除非衡量,否则没有关于绩效的陈述是有意义 (9认同)
  • @Jim Ferrans:我很确定Test1次是来自班级负载.但是,使用双括号初始化的结果是必须应对类加载.我相信大多数用例是双支撑init.对于一次性初始化,测试在这种初始化的典型用例的条件下更接近.我相信每次测试的多次迭代会使执行时间间隔变小. (8认同)
  • "更不用说增加所需的磁盘空间" - 忘记磁盘空间; 想想所有这些匿名类定义将对你的[PermGen](http://www.integratingstuff.com/2011/07/24/understanding-and-avoiding-the-java-permgen-space-error/)空间做些什么!所有这些匿名类定义都在那里加载,并且它们不会被卸载.这使得在除静态初始化程序(或等效的"一次性"作用域)之外的任何上下文中使用双括号初始化都会导致内存泄漏. (5认同)
  • 现在有了 Java 9,你可以使用 `List&lt;String&gt; l1 = new ArrayList&lt;&gt;(List.of("Hello", "World"));` 甚至 `List&lt;String&gt; l1 = List.of("Hello ", "World");`,如果你不需要 `ArrayList`,但在后一种情况下,你可以使用 `List&lt;String&gt; l1 = Arrays.asList("Hello", "World"); ` 从 Java 5 开始。 (3认同)
  • @Niel Coffey:是的,因为每个双括号初始化都是一个单独的匿名内部类,它会为每次初始化创建一个新类. (2认同)
  • 实际上那和一些permgen空间.但他们是如此微小的课程,我怀疑这是一个很大的问题. (2认同)
  • 如果你考虑运行而不是加载性能没有区别,请参阅我的回答. (2认同)
  • @MichaelMyers - 只需要查看与[耗尽PermGen空间]相关的问题数量(http://stackoverflow.com/questions/88235/dealing-with-java-lang-outofmemoryerror-permgen-space-error)看到你真的_don't_想要在"如此微小的类"上不必要地浪费它,在任何服务器环境中都是如此.或者预期应用程序长时间运行的任何其他上下文,通常,由于内存泄漏,_not_崩溃. (2认同)
  • 您的Test2无效测试.由于java优化,您的测试中只能有0.例如,您应该在Test2中打印已创建列表的大小. (2认同)
  • @MCEmperor:如果你告诉编译器你想要一个子类,编译器会给你一个子类。您可以确定 `lst.getClass()==ArrayList.class` 的计算结果为 `false`。不允许优化编译器更改程序的语义,即使程序员很可能实际上并不想要该语义。 (2认同)

Thi*_*ilo 102

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

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

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

            @Override
            boolean add(String s){

            }

        };
    }
Run Code Online (Sandbox Code Playgroud)

  • 对包含类的隐藏引用非常好.在原始示例中,实例初始化程序正在调用新HashSet <String>的add()方法,而不是Test.this.add().这告诉我,其他事情正在发生.Nathan Kitchen建议,HashSet <String>是否有一个匿名的内部类? (5认同)

Luk*_*der 50

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

除了语法相当不寻常而且不是真正的惯用语(当然,品味是值得商榷的)之外,您在应用程序中不必要地创建了两个重要问题,我最近在这里更详细地讨论了这些问题.

你创造了太多的匿名课程

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

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");
        }});
    }});
}};
Run Code Online (Sandbox Code Playgroud)

...将产生这些类:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class
Run Code Online (Sandbox Code Playgroud)

这对你的类加载器来说是一个相当大的开销 - 什么都不是!当然,如果你这样做一次,它将不需要太多的初始化时间.但是,如果你在整个企业应用程序中执行此操作20,000次...所有堆内存只是为了一点"语法糖"?

你可能会造成内存泄漏!

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

返回的Map现在将包含对封闭实例的引用ReallyHeavyObject.你可能不想冒这样的风险:

记忆泄漏就在这里

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

你可以假装Java有地图文字

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

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};
Run Code Online (Sandbox Code Playgroud)

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

  • 拯救小猫!好答案! (5认同)

Edd*_*die 35

参加以下测试课程:

public class Test {
  public void test() {
    Set<String> flavors = new HashSet<String>() {{
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }};
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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");
      }
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

这对我来说看起来效率不高.如果我担心这样的表现,我会对它进行分析.上面的代码回答了你的问题#2:你在内部类的隐式构造函数(和实例初始化程序)中,所以" this"指的是这个内部类.

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

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}
Run Code Online (Sandbox Code Playgroud)

您也可以使用类似的语法(没有单词" 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你没有一个默认的构造函数,那么之间的代码块{,并}变成由编译器的构造函数.考虑到这一点,解开双支撑代码:

public void test() {
  Set<String> flavors = new HashSet<String>() {
      {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
  };
}
Run Code Online (Sandbox Code Playgroud)

最内部大括号之间的代码块由编译器转换为构造函数.最外面的大括号分隔匿名内部类.要把这一切变成非匿名的最后一步:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}
Run Code Online (Sandbox Code Playgroud)

出于初始化目的,我会说没有任何开销(或者很小以至于可以忽略).但是,每次使用flavors都不会反对HashSet而是反对MyHashSet.这可能是一个很小的(很可能是微不足道的)开销.但是,在我担心之前,我会再说一下.

同样,对于你的问题#2,上面的代码是双括号初始化的逻辑和显式等价,它使" this"指的是显而易见的:对于扩展的内部类HashSet.

如果您对实例初始化程序的详细信息有疑问,请查看JLS文档中的详细信息.


bes*_*sss 35

易泄漏

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

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}
Run Code Online (Sandbox Code Playgroud)

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

  • 幸运的是,从Java 8开始,PermGen不再是一件事.我想,仍有影响,但不是一个可能非常模糊的错误消息. (3认同)
  • @Joey,如果内存由GC(perm gen)直接管理,则会产生零差异.元空间中的泄漏仍然是一个漏洞,除非元数量有限,否则不会有像oom_killer这样的东西会出现OOM(超出perm gen). (2认同)

Pet*_*rey 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

版画

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
Run Code Online (Sandbox Code Playgroud)

  • 没有区别,只是如果过度使用DBI,你的PermGen空间会蒸发掉.至少,除非你[设置一些不起眼的JVM选项](http://stackoverflow.com/questions/3334911/what-does-jvm-flag-cmsclassunloadingenabled-actually-do)允许类卸载和垃圾收集PermGen空间.鉴于Java作为服务器端语言的流行,内存/ PermGen问题至少需要提及. (2认同)
  • 难道不应该将`compareCollections`中的条件与`||`而非`&amp;&amp;`相结合吗?使用“ &amp;&amp;”似乎不仅在语义上是错误的,而且还抵消了衡量性能的意图,因为只有第一个条件会被测试。此外,智能优化器可以识别条件在迭代过程中永远不会改变。 (2认同)

Nat*_*Nat 16

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

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}
Run Code Online (Sandbox Code Playgroud)

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

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


Pau*_*rie 9

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

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

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");
Run Code Online (Sandbox Code Playgroud)

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

  • 但请注意asList:返回的列表不支持添加或删除元素.每当我使用asList时,我将结果列表传递给构造函数,如`new ArrayList <String>(Arrays.asList("vanilla","strawberry","chocolate"))`以解决这个问题. (6认同)

Nei*_*fey 7

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

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

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

至于(3),这实际上取决于谁维护你的代码,我想.如果您事先不知道这一点,那么我建议使用的基准是"你在JDK的源代码中看到这个吗?" (在这种情况下,我不记得看到许多匿名初始化者,当然不会出现这是匿名类的唯一内容).在大多数中等规模的项目中,我认为你真的需要你的程序员在某些时候理解JDK源代码,所以在那里使用的任何语法或习语都是"公平游戏".除此之外,我会说,如果你能控制谁维护代码,那么就培养人们的语法,否则评论或避免.


dim*_*414 5

双括号初始化是不必要的hack,可能导致内存泄漏和其他问题

没有合理的理由使用此“技巧”。番石榴提供了不错的不可变集合,其中包括静态工厂和构建器,使您可以使用干净,易读且安全的语法在声明了集合的地方进行填充。

问题中的示例变为:

Set<String> flavors = ImmutableSet.of(
    "vanilla", "strawberry", "chocolate", "butter pecan");
Run Code Online (Sandbox Code Playgroud)

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

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

现在,Error-Prone 标记此反模式

  • -1. 尽管有一些有效的观点,但这个答案归结为“如何避免生成不必要的匿名类?使用具有更多类的框架!” (3认同)