Java中的双括号初始化是什么?

395

Java中的双括号初始化语法({{ ... }})是什么?


2
请见 https://dev59.com/IXM_5IYBdhLWcg3wdy1g#1372124 - skaffman
2
另请参阅https://dev59.com/pnNA5IYBdhLWcg3wh-YC - Jim Ferrans
13
双括号初始化是一个非常危险的特性,应该谨慎使用。它可能会破坏等式契约并引入棘手的内存泄漏问题。这篇文章详细描述了相关信息。 - Andrii Polunin
Andrii发布的链接已经失效,但我自己写了一篇关于它的博客文章:不要使用双括号初始化技巧 - Jesper
13个回答

389

双括号初始化会创建一个派生自指定类的匿名类(外侧大括号),并在该类中提供了一个初始化块(内部大括号)。例如:

new ArrayList<Integer>() {{
   add(1);
   add(2);
}};
请注意,使用这种双括号初始化的效果是创建了匿名内部类。所创建的类对周围外部类有一个隐含的this指针。虽然通常不会出现问题,但在某些情况下(例如序列化或垃圾回收时)可能会引起麻烦,因此值得注意。

15
感谢您澄清了内部括号和外部括号的含义。我一直在想为什么会突然出现两个有特殊意义的括号,实际上它们只是普通的Java构造,只是以某种神奇的新技巧呈现。这类事情让我对Java语法产生疑问。如果您不是专家,阅读和编写就可能非常棘手。 - jackthehipster
9
许多编程语言中都存在类似于“魔法语法”的东西,例如几乎所有类C语言都支持在for循环中的“x --> 0”这样的“走向0”的语法,实际上它只是“x-- > 0”语法的一种奇怪的空格排列方式。 - Joachim Sauer
38
我们可以得出结论,“双括号初始化”并不存在于其本身,它只是创建一个匿名类和一个初始化块的组合,一旦结合起来,看起来像一个语法结构,但实际上并不是。 - MC Emperor
谢谢!Gson在使用双括号初始化时返回null,因为它使用了匿名内部类。 - Pradeep AJ- Msft MVP
1
永远不要使用这种可憎的东西。永远不要。 - John LaBarge

366

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

除了语法相当不寻常且不太习惯(当然,口味是有争议的),你还会在应用程序中不必要地创建两个重大问题,我最近在这里更详细地讨论了这个问题

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

有些人可能会发现这在语法上很刺激。


13
“你创建了太多的匿名类”- 从(比如)Scala创建匿名类的方式来看,我不确定这是否是一个主要问题。 - Brian Agnew
5
将HashMap初始化为{{...}}并声明为static字段,是否仍然是一种有效且不错的静态映射声明方式?这样做不应该会有任何可能的内存泄漏,只有一个匿名类和没有封闭实例引用,对吗? - lorenzo-s
12
明白了,2)和3)不适用,只有1)。幸运的是,Java 9终于推出了Map.of()来实现这个目的,所以那将是更好的解决方案。 - Lukas Eder
3
值得注意的是,内部地图也引用外部地图,因此间接地引用了ReallyHeavyObject。此外,匿名内部类捕获了类体内使用的所有局部变量,因此如果您在初始化集合或地图时不仅使用常量,内部类实例将捕获所有这些变量,并在实际从集合或地图中删除时继续引用它们。因此,在这种情况下,这些实例不仅需要两倍于必要的引用内存,而且在这方面还存在另一个内存泄漏问题。 - Holger
5
@JacobEckel,现在已经是2021年了,Java有了与Map字面值相近的东西,以此回答为例:Map source = Map.of("firstName", "John", "lastName", "Smith", "organizations", Map.of("0", Map.of("id", "1234"), "abc", Map.of("id", "5678")))(自Java 9起),这确实生成一个不可变的映射。 - Holger
显示剩余4条评论

55
  • 第一个大括号创建了一个新的匿名内部类。
  • 第二组大括号创建了一个实例初始化器,就像类中的静态块一样。

例如:

   public class TestHashMap {
    public static void main(String[] args) {
        HashMap<String,String> map = new HashMap<String,String>(){
        {
            put("1", "ONE");
        }{
            put("2", "TWO");
        }{
            put("3", "THREE");
        }
        };
        Set<String> keySet = map.keySet();
        for (String string : keySet) {
            System.out.println(string+" ->"+map.get(string));
        }
    }
    
}

它是如何工作的

第一个大括号创建一个新的匿名内部类。这些内部类能够访问其父类的行为。所以,在我们的案例中,我们实际上创建了HashSet类的子类,因此这个内部类能够使用put()方法。

第二组大括号只是实例初始化器。如果你记得核心Java概念,那么你可以很容易地将实例初始化器块与静态初始化器关联起来,因为它们有相似的结构。唯一的区别是静态初始化器添加了static关键字,并且只运行一次;无论你创建多少对象,它都只运行一次。

更多信息


27

关于双括号初始化的有趣应用,请参见这里Dwemthy's Array in Java

一个摘录

private static class IndustrialRaverMonkey
  extends Creature.Base {{
    life = 46;
    strength = 35;
    charisma = 91;
    weapon = 2;
  }}

private static class DwarvenAngel
  extends Creature.Base {{
    life = 540;
    strength = 6;
    charisma = 144;
    weapon = 50;
  }}

现在,准备好迎接 BattleOfGrottoOfSausageSmells 和 … 厚切培根!


25

我认为强调Java中不存在"双括号初始化"是很重要的。Oracle网站上没有这个术语。在这个示例中,有两个功能一起使用:匿名类和初始化块。看起来旧的初始化块已经被开发人员遗忘,并在这个主题中造成了一些困惑。Oracle文档的引用:

实例变量的初始化程序块看起来与静态初始化程序块完全相同,但没有static关键字:

{
    // whatever code is needed for initialization goes here
}

17

1- 双括号初始化并不存在:
我想指出的是,双括号初始化并不存在。只存在普通的传统一括号初始化块。第二个括号块与初始化无关。回答中提到这两个括号会初始化某些东西,但实际上不是这样的。

2- 不仅用于匿名类,而是所有类:
几乎所有答案都谈到了在创建匿名内部类时使用该技术。我认为,读者会得出这只用于创建匿名内部类的印象。但实际上,它在所有类中都可以使用。读完这些答案,似乎这是一种全新的专门用于匿名类的特殊功能,我认为这是误导。

3- 目的仅仅是把括号放在一起,并没有新的概念:
此外,这个问题谈论的是第二个开括号紧跟在第一个开括号后的情况。通常,在正常的类中,两个括号之间会有一些代码,但这完全相同。因此,这只是一个括号的问题。因此,我认为我们不应该说这是一种新的令人兴奋的东西,因为这是我们都知道的事情,只是在括号之间写了一些代码。我们不应该创造一个名为“双括号初始化”的新概念。

4- 创建嵌套的匿名类与两个括号无关:
我不同意使用过多匿名类的论点。你并不是因为初始化块创建它们,而只是因为你需要它们。即使没有使用两个括号初始化,这些匿名类也会被创建,因此,即使没有初始化,这些问题仍然会发生... 初始化不是创建初始化对象的因素。

此外,我们不应该谈论使用这个不存在的东西“双括号初始化”或甚至正常的一括号初始化所创建的问题,因为所描述的问题只存在于创建匿名类时,因此与原始问题无关。但是所有答案都会给读者留下这不是创建匿名类的错误,而是这个邪恶(不存在)的“双括号初始化”。


12

为了避免双括号初始化带来的所有负面影响,如:

  1. 破坏“equals”兼容性。
  2. 在使用直接赋值时不进行检查。
  3. 可能导致内存泄漏。

做以下事情:

  1. 专门为双括号初始化创建单独的“Builder”类。
  2. 声明带有默认值的字段。
  3. 将对象创建方法放在该类中。

示例:

public class MyClass {
    public static class Builder {
        public int    first  = -1        ;
        public double second = Double.NaN;
        public String third  = null      ;

        public MyClass create() {
            return new MyClass(first, second, third);
        }
    }

    protected final int    first ;
    protected final double second;
    protected final String third ;

    protected MyClass(
        int    first ,
        double second,
        String third
    ) {
        this.first = first ;
        this.second= second;
        this.third = third ;
    }

    public int    first () { return first ; }
    public double second() { return second; }
    public String third () { return third ; }
}

使用方法:

MyClass my = new MyClass.Builder(){{ first = 1; third = "3"; }}.create();

优点:

  1. 使用简单。
  2. 不会破坏“equals”兼容性。
  3. 可以在创建方法中执行检查。
  4. 没有内存泄漏。

缺点:

  • 无。

因此,我们拥有了最简单的Java Builder模式。

请在Github上查看所有样例:java-sf-builder-simple-example


1
MyClass my = new MyClass.Builder().first(1).third("3").create(); 这种写法至少和你的变量创建方式一样简单,而且不需要创建匿名子类。同时还可以立即验证值的正确性。 - Holger
MyClass my = new MyClass(); my.first(1); my.third(3); 我认为这种写法更加简单且更优秀。您不需要使用构建器类,从而重要地简化了代码。但是,我肯定不是一个时髦的人。 - AgilePro

5

正如 @Lukas Eder 所指出的,应避免使用双括号初始化集合。

这会创建一个匿名内部类,由于所有内部类都会保留对其父实例的引用,如果这些集合对象被多个对象引用而不仅仅是声明它们的对象,则可能 - 99% 的情况下 - 防止垃圾回收。

Java 9 引入了方便的方法 List.ofSet.ofMap.of,应该使用它们代替。它们比双括号初始化更快、更高效。


4

它是一种用于初始化集合的快捷方式,除此之外还有其他用途。了解更多...


2
那是其中一个应用,但绝不是唯一的。 - skaffman

3
你可以将一些Java语句作为循环来初始化集合:
List<Character> characters = new ArrayList<Character>() {
    {
        for (char c = 'A'; c <= 'E'; c++) add(c);
    }
};

Random rnd = new Random();

List<Integer> integers = new ArrayList<Integer>() {
    {
         while (size() < 10) add(rnd.nextInt(1_000_000));
    }
};

但是这种情况会影响性能,请查看这个讨论


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