ChatGPT解决这个技术问题 Extra ChatGPT

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

Hidden Features of Java 中,最重要的答案提到了 Double Brace Initialization,其语法 非常 很诱人:

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、可变参数方法、Google Collections 和建议的 Java 7 Collection 文字来实现相同的效果。 Scala、JRuby 和 Groovy 等较新的 JVM 语言也为列表构造提供了简洁的符号,并与 Java 很好地互操作。鉴于 DBI 会使类路径变得混乱,会稍微减慢类加载速度,并使代码更加晦涩难懂,我可能会回避它。但是,我打算向一个刚刚获得 SCJP 并且喜欢关于 Java 语义的善意的较量的朋友提出这个建议! ;-) 感谢大家!

7/2017:双括号初始化的 Baeldung has a good summary 并认为它是一种反模式。

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

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

这肯定是要走的路。如果您无法使用较早的版本,请查看 Google Collections' ImmutableSet

我在这里看到的代码味道是,天真的读者会认为 flavorsHashSet,但可惜它是一个匿名子类。
如果您考虑运行而不是加载性能没有区别,请参阅我的答案。
我喜欢你创建了一个摘要,我认为这对你和社区来说都是一个值得的练习。
在我看来这并不模糊。读者应该知道双...哦等等,@ElazarLeibovich 已经在 his comment 中说过。双括号初始化器本身并不作为语言结构存在,它只是匿名子类和实例初始化器的组合。唯一的问题是,人们需要意识到这一点。
Java 9 提供的 Immutable Set Static Factory Methods 可能会在某些情况下替代 DCI 的使用:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;

H
Hearen

当我对匿名内部类太着迷时,这就是问题所在:

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 虚拟机在使用它们时需要读取所有这些类,这可能会导致 bytecode verfication 进程等一些时间。更不用说为了存储所有这些 class 文件而增加了所需的磁盘空间。

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

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

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

它看起来像是 Java 的“隐藏”功能,但它只是对以下内容的重写:

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

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

所以它基本上是一个 instance initialization block,它是 anonymous inner class 的一部分。

Joshua Bloch 对 Project CoinCollection Literals proposal 大致如下:

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

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

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

可悲的是,它 didn't make its way 既没有进入 Java 7 也没有进入 Java 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);
  }
}

请注意,初始化 1000 个 ArrayList 和扩展 ArrayList 的 1000 个匿名内部类所用的时间是使用 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 文件。


“可能”是有效词。除非经过衡量,否则任何关于性能的陈述都是有意义的。
你已经完成了如此出色的工作,我几乎不想这么说,但是 Test1 时间可能会被类负载所支配。看到有人在 for 循环中运行每个测试的单个实例 1,000 次,然后在第二个 for 循环中再次运行 1,000 或 10,000 次并打印出时间差(System.nanoTime())会很有趣。第一个 for 循环应该通过所有的热身效果(JIT、类加载等)。不过,这两个测试都模拟了不同的用例。我明天在工作中尝试运行它。
@Jim Ferrans:我相当确定 Test1 时间来自类负载。但是,使用双括号初始化的后果是不得不应付类负载。我相信双括号初始化的大多数用例。对于一次性初始化,测试在条件上更接近此类初始化的典型用例。我相信每个测试的多次迭代会使执行时间差距更小。
这证明了 a) 双括号初始化速度较慢,b) 即使您执行 1000 次,您也可能不会注意到差异。这也不是内部循环的瓶颈。在最糟糕的情况下,它会施加很小的一次性处罚。
如果使用 DBI 使代码更具可读性或表现力,那么就使用它。它增加了 JVM 必须执行的工作这一事实本身并不是反对它的有效论据。如果是这样,那么我们也应该担心额外的辅助方法/类,而是更喜欢方法较少的大型类......
J
Jim Ferrans

迄今为止尚未指出的这种方法的一个属性是,因为您创建了内部类,所以整个包含类都被捕获在其范围内。这意味着只要您的 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){

            }

        };
    }

对包含类的隐藏引用非常好。在原始示例中,实例初始化程序正在调用新 HashSet 的 add() 方法,而不是 Test.this.add()。这向我表明正在发生其他事情。正如 Nathan Kitchen 所建议的那样,HashSet 是否存在匿名内部类?
如果涉及数据结构的序列化,对包含类的引用也可能很危险。包含类也将被序列化,因此必须是可序列化的。这可能会导致难以理解的错误。
而且不仅仅是this$0。当这个技巧与非常量值一起使用时,即使从集合中删除相应的元素,访问变量的值也会被捕获并保持引用。在最极端的情况下,您最终可能会得到一个空集合,其中包含数百个对其初始化对象的引用。
L
Lukas Eder

每次有人使用双括号初始化时,都会杀死一只小猫。

除了语法相当不寻常并且不是真正地道(当然,品味是有争议的)之外,您还不必要地在您的应用程序中创建了两个重大问题,which I've just recently blogged about in more detail here

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 封闭实例的引用。您可能不想冒险:

https://i.stack.imgur.com/DJ8ov.png

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

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

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

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

有些人可能会觉得这在句法上很刺激。


救救小猫!好答案!
E
Eddie

参加以下测试课程:

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 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。这可能有一个很小的(而且很可能可以忽略不计)开销。但同样,在我担心它之前,我会分析它。

同样,对于您的问题 #2,上面的代码是双括号初始化的逻辑和显式等价物,它使“this”指的地方很明显:指向扩展 HashSet 的内部类。

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


埃迪,很好的解释。如果 JVM 字节码和反编译一样干净,那么执行速度会足够快,尽管我有点担心额外的 .class 文件混乱。我仍然很好奇为什么实例初始化程序的构造函数将“this”视为新的 HashSet 实例而不是 Test 实例。这只是在最新的 Java 语言规范中明确指定的行为以支持该习语吗?
我更新了我的答案。我遗漏了 Test 类的样板,这引起了混乱。我把它放在我的答案中,以使事情更明显。我还提到了这个习惯用法中使用的实例初始化程序块的 JLS 部分。
@Jim“this”的解释不是特例;它只是指最内层封闭类的实例,它是 HashSet 的匿名子类。
很抱歉四年半后加入。但是反编译的类文件(您的第二个代码块)的好处是它不是有效的 Java!它有 super() 作为隐式构造函数的第二行,但它必须排在第一位。 (我已经测试过了,它不会编译。)
@chiastic-security:有时反编译器会生成无法编译的代码。
3
3 revs, 3 users 90%

容易泄漏

我决定加入。性能影响包括:磁盘操作 + 解压缩(用于 jar)、类验证、perm-gen 空间(用于 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"));


幸运的是,从 Java 8 开始,PermGen 不再是一个东西。我猜仍然有影响,但没有一个可能非常模糊的错误消息。
@Joey,如果内存是否由 GC(perm gen)直接管理,则差异为零。元空间中的泄漏仍然是泄漏,除非元受到限制,否则不会出现像 Linux 中的 oom_killer 这样的 OOM(超出 perm gen)。
J
Jim Ferrans

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

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

没有区别,只是如果过度使用 DBI,您的 PermGen 空间将会蒸发。至少,除非您 set some obscure JVM options 允许 PermGen 空间的类卸载和垃圾收集,否则它会。鉴于 Java 作为服务器端语言的流行,内存/PermGen 问题至少值得一提。
@aroth 这是一个很好的观点。我承认在 16 年的 Java 工作中,我从来没有在一个必须调整 PermGen(或 Metaspace)的系统上工作。对于我工作过的系统,代码的大小总是保持相当小。
compareCollections 中的条件不应该与 || 而不是 && 组合吗?使用 && 似乎不仅在语义上是错误的,它还抵消了衡量性能的意图,因为根本只测试第一个条件。此外,智能优化器可以识别出迭代期间条件永远不会改变。
@aroth 只是作为更新:从 Java 8 开始,VM 不再使用任何 perm-gen。
@AngelO'Sphere permgen 消失了,但 Metaspace 是它的继任者(具有一些不同的行为/限制),但类定义仍然存在于内存中的某个地方——它不是免费的。
N
Nat

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

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

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

至于成语的晦涩之处,我一直在生产代码中遇到它并使用它。我会更担心那些被允许编写生产代码的成语弄糊涂的程序员。


哈! ;-) 我实际上是从 1.2 天开始返回 Java 的 Rip van Winkle(我在 evolution.voxeo.com 用 Java 编写了 VoiceXML 语音 Web 浏览器)。学习泛型、参数化类型、集合、java.util.concurrent、新的 for 循环语法等很有趣。现在它是一种更好的语言。就您的观点而言,尽管 DBI 背后的机制起初可能看起来晦涩难懂,但代码的含义应该很清楚。
P
Paul Morie

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

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

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

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


Arrays.asList() 是我通常使用的,但你是对的,这种情况主要出现在单元测试中;真正的代码会根据 DB 查询、XML 等来构造列表。
但请注意 asList:返回的列表不支持添加或删除元素。每当我使用 asList 时,我都会将结果列表传递给像 new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate")) 这样的构造函数来解决这个问题。
d
dimo414

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

没有正当理由使用这个“技巧”。 Guava 提供了不错的 immutable collections,其中包括静态工厂和构建器,允许您在集合中以干净、可读且安全 的语法声明它的位置。

问题中的示例变为:

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

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

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

Error-Prone 现在 flags this anti-pattern


-1。尽管有一些有效的观点,但这个答案归结为“如何避免生成不必要的匿名类?使用框架,甚至更多的类!”
我会说它归结为“使用正确的工具完成工作,而不是可能导致应用程序崩溃的黑客”。 Guava 是一个非常常见的应用程序库(如果你不使用它,你肯定会错过它),但即使你不想使用它,你也可以并且仍然应该避免双括号初始化。
双括号初始化究竟会如何导致内存泄漏?
@AngelO'Sphere DBI 是一种创建 inner class 的混淆方式,因此保留对其封闭类的隐式引用(除非仅在 static 上下文中使用过)。我的问题底部的 Error-Prone 链接进一步讨论了这一点。
我会说这是一个品味问题。并没有什么真正令人困惑的。
N
Neil Coffey

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

我会更多地从您是否想用大量匿名类“混乱”的角度来看待这个问题。作为一个粗略的指导,考虑使用这个习语,就像你使用匿名类一样,事件处理程序。

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

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


L
Lawrence Dol

我第二个 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;
    }

为什么?新对象将在 eden 空间中创建,因此只需要添加两个或三个指针即可实例化。 JVM 可能会注意到它永远不会超出方法范围,因此将其分配在堆栈上。
是的,它最终可能比该代码更有效(尽管您可以通过告诉 HashSet 建议的容量来改进它 - 记住负载因子)。
好吧,无论如何,HashSet 构造函数都必须进行迭代,因此它的效率不会降低。为重用而创建的库代码应始终努力做到最好。
p
pablisco

我正在对此进行研究,并决定进行比有效答案提供的更深入的测试。

这是代码:https://gist.github.com/4368924

这是我的结论

我惊讶地发现,在大多数运行测试中,内部启动实际上更快(在某些情况下几乎翻倍)。当处理大量数字时,好处似乎消失了。有趣的是,在循环上创建 3 个对象的情况失去了它的好处,比其他情况更快地用完。我不确定为什么会发生这种情况,应该进行更多测试以得出任何结论。创建具体的实现可能有助于避免重新加载类定义(如果发生这种情况)。但是,很明显,在大多数情况下,对于单个项目构建来说,它观察到的开销并不大,即使数量很大。一个挫折是这样一个事实,即每个双括号初始化都会创建一个新的类文件,它将整个磁盘块添加到我们的应用程序的大小(或压缩时大约 1k)。占地面积小,但如果在许多地方使用它可能会产生影响。使用此 1000 次,您可能会向您的应用程序添加整个 MiB,这可能与嵌入式环境有关。我的结论?只要不被滥用就可以使用。

让我知道你的想法 :)


这不是一个有效的测试。代码在不使用对象的情况下创建对象,这允许优化器省略整个实例的创建。唯一剩下的副作用是随机数序列的推进,无论如何,在这些测试中,其开销超过了其他任何东西。
E
Eric Woodruff

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


C
Community

Mario Gleichman describes 如何使用 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 = Maps.newHashMap(); List 动物 = Lists.immutableList("cat", "dog", "horse");

2014 年 7 月 10 日:如果它可以像 Python 一样简单:

动物 = ['猫','狗','马']

2/21/2020:在 Java 11 中,您现在可以说:

动物 = List.of(“猫”、“狗”、“马”)


M
MC Emperor

这将为每个成员调用 add()。如果您能找到一种更有效的方法将项目放入哈希集中,请使用它。请注意,如果您对此敏感,内部类可能会产生垃圾。在我看来,上下文似乎是 new 返回的对象,即 HashSet。如果您需要问... 更有可能:追随您的人是否知道这一点?容易理解和解释吗?如果您对两者都回答“是”,请随意使用它。