Java双括号初始化总是有效吗?

15
我知道这段代码:

Set<String> set = new HashSet<String>() {{
  add("test1");
  add("test2");
}};

真正的含义是:

Set<String> set = new HashSet<String>() {
  {//initializer
    add("test1");
    add("test2");
  }
};

初始化块在构造器块之前被执行。在上面的例子中,add("test1")在构造器被执行之前就被调用了。构造器可能会初始化许多实例字段,以使这个类正常工作。我想知道为什么在构造器之前调用.add() 也能正常工作?是否存在任何可能引起问题的情况?


2
这可能属于“未指定行为”... - 11684
2
有趣的问题。我没有确切的答案,但我认为你在这里做了一个错误的假设。如果你看一下HashSet的构造函数,它会执行以下操作:map = new HashMap<E,Object>(); 而add方法则是这样的:return map.put(e, PRESENT)==null; 如果你的假设是正确的,那么这将导致NPE。 - Daniel Kaplan
考虑这个“模式”是否真的值得麻烦,这样想肯定不是错的。 - Lukas Eder
4个回答

17

你遗漏了一个细节,这个细节可以解释这个问题。

首先,让我们回顾一下初始化过程的第3至5步(摘要):initialization procedure

3. 调用超类构造函数
4. 调用实例初始化器
5. 调用构造函数主体

你遗漏的细节是,你的表达式不仅仅创建了HashSet类的新实例,实际上它创建了HashSet的匿名子类的新实例。(我相信这在第15.9.1节中有规定。)

由于你没有声明构造函数,因此默认构造函数被使用。但在此之前,HashSet超类的构造函数已经完成。

所以,总结一下,在你的初始化块运行之前,HashSet构造函数已经完成。


谢谢,Samuel。你恰好解释了缺失的那一部分。那段代码块没有初始化HashSet,而是一个匿名子类的HashSet。 - user926958
@user926958,在这个例子中很容易忽略一个细节。如果你从像Set这样的接口或抽象类派生,你将需要添加一些方法定义,这将更明显地表明正在定义一个新类。 - Samuel Edwin Ward

6
这个假设是错误的:
initializer块在构造器块之前被执行。
因为在这种情况下,initializer块是构造器块的一部分。 文档明确说明:
Java编译器会将initializer块复制到每个构造器中。因此,这种方法可以用于在多个构造器之间共享代码块。
我认为你可能混淆了静态initializer。

看看我在原问题上的评论。块不是必须在构造函数之前出现吗?并不是你说的反话,我只是想更好地理解。 - Daniel Kaplan
顺序看起来像是在构造函数之前进行初始化,至少我是从这里得到的:https://dev59.com/w3I-5IYBdhLWcg3wHUjP - user926958
1
我不是有意冒犯,但这并不是发生的事情;初始化块在调用super之后但在构造函数的其余部分之前被调用,这在大多数情况下会导致问题;他在HashMap类的内联扩展中完成,这就是为什么它总是有效的。 - Andrew White
1
这是一个匿名子类,因此OP的初始化块将被转换为一个带有构造函数的类,其主体看起来像super(); add("test1"); add("test2"); - 11684
你认为这段代码是做什么的?它是一个 HashSet 的匿名子类。 - mprivat

4
实例初始化程序在对象构造后立即执行。您基本上是创建 HashSet 的内联扩展,然后“刚刚创建”它并添加了两个项。
这是测试中模拟对象的常见用法模式,例如在 JMock 中,但也有其他方便的用途。
希望这可以帮助到您。

2
它不是在对象构造完成后执行的。它是在对象构造期间执行的,在超类构造函数和构造函数体之间执行。 - Geoff Reedy

3

我认为这是一种不好的做法,因为它会创建无意义的子类,可能会影响应用程序的内存使用和性能。但无论如何,该程序是正确的,因为在实例初始化程序运行之前会调用超类构造函数。因此,当您的初始化程序运行时, HashSet 构造函数已运行,因此调用 add 将起作用。


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