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


823

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,varargs方法,Google Collections和建议的Java 7 Collection文字可以实现相同的效果。较新的JVM语言(例如Scala,JRuby和Groovy)还为列表构建提供了简洁的符号,并且可以与Java很好地互操作。鉴于DBI会使类路径混乱,使类加载速度变慢,并使代码更加模糊,我可能会回避它。但是,我打算将这个介绍给一个刚刚获得SCJP并且喜欢Java语义的自然风趣的朋友!;-) 谢谢大家!

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

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

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

那肯定是要走的路。如果您使用的是早期版本,请查看Google Collections的ImmutableSet


33
我在这里看到的代码味道是,天真的读者希望flavorsHashSet,但是可惜它是一个匿名子类。
Elazar Leibovich

6
如果您考虑运行而不是加载性能,则没有区别,请参阅我的答案。
彼得·劳瑞

4
我喜欢您创建了一个摘要,我认为这对您和您的社区都是一个有价值的练习。
帕特里克·墨菲

3
我认为这并不晦涩。读者应该知道这是双重的... ...等等,@ ElazarLeibovich在评论中已经说过。双括号初始化器本身不作为语言构造存在,它只是匿名子类和实例初始化器的组合。唯一的是,人们需要意识到这一点。
MC皇帝

8
Java 9提供了不可变集静态工厂方法,这些方法在某些情况下可以代替DCI的使用:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

Answers:


607

这是我对匿名内部类太过迷恋的问题:

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 提出Collection Literals提案大致如下:

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

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

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

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


实验

下面是简单的实验我已经测试-让1000个ArrayLists的元素"Hello",并"World!"通过加入到他们add的方法,使用两种方法:

方法1:双括号初始化

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个ArrayListS和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 ms。

同时,ArrayList初始化执行时间为0 ms。当然,应该考虑计时器分辨率,但是很可能在15毫秒以下。

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

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


10
“可能”是有效词。除非进行衡量,否则没有任何关于性能的陈述是有意义的。
实例猎人

16
您做得非常好,我几乎不想这么说,但是Test1时代可能被类负载所支配。有趣的是,看到某人在一个for循环中运行了每个测试的单个实例,比如说运行1000次,然后在第二次运行1000或10,000次for循环中再次运行它,并打印出时间差(System.nanoTime())。第一个for循环应该克服所有的预热效果(例如,JIT,classload)。但这两个测试都对不同的用例进行建模。我明天尝试在工作中运行。
吉姆·弗兰斯

8
@吉姆·弗兰斯(Jim Ferrans):我相当确定Test1的时间来自于课堂学习。但是,使用双括号初始化的结果是必须应付类负载。我相信大多数情况下都需要双括号初始化。如果是一次性初始化,则在某种程度上更接近于这种初始化的典型用例。我相信每个测试的多次迭代将使执行时间差距变小。
coobird

73
这证明了:a)双括号初始化较慢,并且b)即使执行1000次,您也可能不会注意到差异。而且,这也不可能是内部循环中的瓶颈。它对“最糟糕”处以微小的一次性罚款。
迈克尔·迈尔斯

15
如果使用DBI使代码更具可读性或表现力,请使用它。它增加了JVM必须执行的工作这一事实本身并不是反对它的有效论据。如果是这样,那么我们也应该担心额外的辅助方法/类,而宁愿巨大类用更少的方法...
罗杰里奥

105

到目前为止,尚未指出该方法的一个属性是,因为您创建了内部类,所以整个包含类都在其作用域中捕获。这意味着只要您的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()。在我看来,这意味着其他事情正在发生。如Nathan Kitchen建议的那样,HashSet <String>是否有匿名内部类?
吉姆·弗兰斯

如果涉及数据结构的序列化,则对包含类的引用也可能很危险。包含类也将被序列化,因此必须是可序列化的。这会导致模糊的错误。
另一位Java程序员

56

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

除了语法非常不寻常且不是真正惯用的(当然,味道是值得商bat的)之外,您还在应用程序中不必要地创建了两个重要问题,我最近在此处对此进行了详细介绍

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。您可能不想冒险:

内存泄漏就在这里

图片来自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"); }};

有些人可能会从语法上发现这种刺激。


7
救小猫!好答案!
Aris2World

36

参加以下测试课程:

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 <String>实例而不是Test实例感到好奇。这只是在最新的Java语言规范中明确指定的行为来支持该习语吗?
吉姆·弗兰斯

我更新了答案。我忽略了Test类的样板,这引起了混乱。我将其放入答案中以使事情变得更明显。我还提到了此惯用法中使用的实例初始化程序块的JLS部分。
艾迪

1
@Jim对“ this”的解释不是特例;它仅引用最里面的封闭类的实例,该类是HashSet <String>的匿名子类。
弥敦道厨房

很抱歉在四年半后跳入。但是,关于反编译的类文件(您的第二个代码块)的好处是,它不是有效的Java!它super()作为隐式构造函数的第二行,但必须排在第一位。(我已经对其进行了测试,并且无法编译。)
chiastic-security 2014年

1
@ chiastic-security:有时,反编译器会生成无法编译的代码。
艾迪

35

容易泄漏

我决定介入。对性能的影响包括:磁盘操作+解压缩(对于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"));


3
幸运的是,从Java 8开始,PermGen不再是一回事。我想仍然会产生影响,但是没有一个潜在的非常模糊的错误消息。
乔伊

2
@Joey,如果内存是否直接由GC(perm gen)管理,则零差。metaspace中的泄漏仍然是泄漏,除非meta受到限制,否则不会出现linux中的oom_killer之类的OOM(在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
没什么区别,但是如果过度使用DBI,您的PermGen空间将消失。至少,除非您设置了一些晦涩的JVM选项以允许PermGen空间的类卸载和垃圾回收,否则它会。考虑到Java作为服务器端语言的盛行,内存/ PermGen问题至少值得一提。
aroth 2015年

1
@aroth这是一个好点。我承认在使用Java的16年中,我从未在必须调整PermGen(或元空间)的系统上工作,因为我使用的系统代码大小始终保持在很小的范围内。
彼得·劳瑞

2
不应该在条件compareCollections得到与合并||,而不是&&?使用&&似乎不仅在语义上是错误的,而且还抵消了衡量性能的意图,因为将仅测试第一个条件。此外,智能优化器可以识别出条件在迭代过程中永远不会改变。
霍尔格

@aroth作为更新:由于Java 8,VM不再使用任何perm-gen。
Angel O'Sphere,

16

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

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

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

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


哈哈!;-)实际上,我是Rip van Winkle,从1.2天开始返回Java(我在Java的Evolution.voxeo.com上编写了VoiceXML语音Web浏览器)。学习泛型,参数化类型,Collections,java.util.concurrent,新的for循环语法等非常有趣。现在它是一种更好的语言。就您而言,即使乍一看DBI背后的机制似乎还不清楚,但代码的含义应该很清楚。
吉姆·弗兰斯

10

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

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

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

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


1
我通常会使用Arrays.asList(),但是您是对的,这种情况主要发生在单元测试中。真正的代码将根据数据库查询,XML等构造列表。
吉姆·弗兰斯

7
但是要当心asList:返回的列表不支持添加或删除元素。每当使用asList时,我都会将结果列表传递到构造函数中,new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))以解决此问题。
迈克尔·迈尔斯

7

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

从是否要使用大量匿名类来“整理事情”的角度来看,我会更多地看到这个问题。作为一个粗略的指导,请考虑使用该惯用法最多,例如,将匿名类用于事件处理程序。

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

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


5

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

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

问题中的示例变为:

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

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

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

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


-1。尽管有一些有效的观点,但是这个答案可以归结为“如何避免生成不必要的匿名类?使用具有更多类的框架!”
Agent_L

1
我可以说归结为“使用正确的工具完成工作,而不是使应用程序崩溃的黑客”。Guava是一个非常常见的库,供应用程序包括(如果您不使用它,肯定会错过),但是即使您不想使用它,也可以并且应该避免双括号初始化。
dimo414 '18

双括号初始化究竟会导致内存泄漏吗?
Angel O'Sphere,

@ AngelO'Sphere DBI是创建内部类的一种混淆方式,因此保留了对其封闭类的隐式引用(除非仅在static上下文中使用)。我问题底部的Error-Prone链接对此进行了进一步讨论。
dimo414

我会说这是一个品味问题。并没有真正混淆它。
Angel O'Sphere

4

我正在研究此问题,因此决定进行比有效答案所提供的深度测试更多的深度测试。

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

这是我的结论

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

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

但是,很明显,在大多数情况下,即使是大量项目,对于单个项目建筑物也没有太多开销。

一个挫折的事实是,每个双括号启动都会创建一个新的类文件,该文件将整个磁盘块添加到应用程序的大小中(压缩后大约为1k)。占地面积小,但如果在许多地方使用,可能会产生影响。使用此1000次,您可能会向您的应用程序添加整个MiB,这可能与嵌入式环境有关。

我的结论呢?只要不被滥用,就可以使用。

让我知道你的想法 :)


2
那不是有效的测试。该代码在不使用对象的情况下创建了对象,这使优化器可以省去整个实例的创建。唯一剩下的副作用是随机数序列的前进,无论如何,其开销要比这些测试中的其他任何事情都要重要。
Holger 2015年

3

我第二次回答纳特,除了我将使用循环而不是创建循环并立即从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;
    }

1
为什么?新对象将在eden空间中创建,因此只需要添加两个或三个指针即可实例化。JVM可能会注意到它永远不会逃脱方法范围,因此会在堆栈上分配它。
纳特

是的,它最终可能比该代码更有效(尽管您可以通过告知HashSet建议的容量来提高它-记住负载系数)。
汤姆·哈汀

那么,HashSet的构造函数做迭代无论如何,所以它不会是不太有效。为重用而创建的库代码应始终努力做到最好
劳伦斯·多尔

3

虽然此语法很方便,但它也会添加很多this $ 0引用,因为它们会嵌套在一起,并且除非在每个引用上都设置了断点,否则很难将调试步骤逐步引入到初始化程序中。出于这个原因,我只建议将其用于普通的setter(尤其是设置为常量)以及匿名子类无关紧要的地方(例如不涉及序列化)。


3

Mario Gleichman 描述了如何使用Java 1.5泛型函数来模拟Scala List文字,尽管遗憾的是您会遇到不可变的 List。

他定义了这个类:

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

现在是Guava一部分的Google Collections 支持类似的列表构建思路。在这次采访中,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日:在Java 11中,您现在可以说:

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


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

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

  3. 如果您需要询问...更可能:您之后的来者是否知道?容易理解和解释吗?如果您都可以回答“是”,请随时使用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.