Java显式构造函数调用和实例初始化器

4
在Java教程-初始化字段中,有一个关于实例初始化块(Instance Initializer)的描述:
Java编译器将初始化块复制到每个构造函数中。因此,这种方法可以用于在多个构造函数之间共享一段代码。
如果释义正确,则以下代码:
public class ConstructorTest {

    public static void main(String[] args) {
        Child c = new Child();
    }
}

class Parent {
    Parent() {
        System.out.println("Parent non-argument Constructor");
    }
}

class Child extends Parent {

    {
        System.out.println("Child Instance Initialization Block");
    }

    Child() {
        this(2);
        System.out.println("Child no-argument Constructor");

    }

    Child(int i) {
        this(10, i);
        System.out.println("Child 1-argument Constructor");
    }

    Child(int i, int j) {
        System.out.println("Child 2-argument Constructor");
    }
}

输出应该是:
Parent non-parm Constructor
Child Instance Initialization Block
Child 2-argument Constructor
Child Instance Initialization Block
Child 1-argument Constructor
Child Instance Initialization Block
Child no-argument Constructor

但实际输出是:
Parent non-argument Constructor
Child Instance Initialization Block
Child 2-argument Constructor
Child 1-argument Constructor
Child no-argument Constructor

我是否误解了那句话的意思,或者描述不够准确?
还有一个关于显式构造函数调用的疑问:
基于两个基本原则:
- 如果存在,则另一个构造函数的调用必须是构造函数中的第一行。 - 在构造函数内部,使用 this() 调用另一个构造函数,使用 super() 调用直接超类的对应构造函数。
这是否意味着在子类的构造函数中使用 this() 会隐式地删除超类的无参构造函数的默认调用?
感谢回复。

静态初始化块只会执行一次。 - DarthVader
1
@DarthVader:这里没有静态初始化程序。 - Jon Skeet
3个回答

5
编辑:事实证明JLS是准确的,尽管很难阅读。在第12.5节中详细说明了所有内容:
在引用新创建的对象作为结果返回之前,使用以下过程处理指定的构造函数以初始化新对象:
1.将构造函数的参数分配给此构造函数调用的新参数变量。
2.如果此构造函数以同一类中的另一个构造函数的显式构造函数调用(§8.8.7.1)开始(使用this),则使用相同的五个步骤评估参数并递归处理该构造函数调用。如果该构造函数调用突然完成,则出于相同的原因,此过程会突然完成;否则,请继续执行第5步。
3.此构造函数不以同一类中的另一个构造函数的显式构造函数调用(使用this)开始。如果此构造函数不是Object类的构造函数,则此构造函数将以超类构造函数的显式或隐式调用(使用super)开始。使用相同的五个步骤评估参数并递归处理该超类构造函数调用。如果该构造函数调用突然完成,则出于相同的原因,此过程会突然完成。否则,请继续执行第4步。
4.执行此类的实例初始化程序和实例变量初始化程序,按照它们在类的源代码中以从左到右的文本顺序出现的顺序将实例变量初始化程序的值分配给相应的实例变量。如果执行其中任何一个初始化程序导致异常,则不会处理进一步的初始化程序,并且此过程会使用相同的异常突然完成。否则,请继续执行第5步。
5.执行此构造函数的其余部分。如果该执行突然完成,则出于相同的原因,此过程会突然完成。否则,此过程正常完成。
请注意突出显示的部分-执行链接的构造函数,然后我们跳过第4步,该步骤将执行实例初始化程序
事实上,可以从输出中看出,实例和字段初始化程序仅执行一次。
非正式地说,我认为准确地描述该过程是:
1.在同一类中链接构造函数(this(...)),直到达到不以this开头的构造函数体。
2.执行适当的超类构造函数。
3.执行实例变量初始化程序和实例初始化程序。
4.执行“最内部”构造函数的主体。
5.弹出构造函数主体堆栈,直到结束具有“入口”的构造函数。

这是否意味着在子类的构造函数中使用this()将隐式删除超类的无参构造函数调用?

是的,在类的构造函数链中,您保证最终会有一个构造函数隐式或显式地调用super。那就是唯一被调用的超类构造函数。

编辑:请注意,您引用的教程明显是错误的。

示例类:

public class Test {
    {
        System.out.println("Foo");
    }
    
    public Test() {
    }
    
    public Test(int i)  {
        this();
    }
}

javap -c的输出结果:

public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1  // Method java/lang/Object."<init>": ()V
       4: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3  // String Foo
       9: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: return

  public Test(int);
    Code:
       0: aload_0
       1: invokespecial #5                  // Method "<init>":()V
       4: return
}

你会发现,Test(int) 的构造函数中并没有实例构造函数的代码。

基本上,只有直接调用超类构造函数的构造函数才会将实例初始化程序代码复制到其中。所有其他构造函数都将通过调用超类构造函数而导致实例初始化程序代码被执行,当然。


但在第二步中,它说:“...继续执行第5步”,这意味着步骤4“执行实例初始化程序”被明确跳过,“如果此构造函数以显式构造函数调用(this())开头”,因此实例初始化程序仅在每个构造函数体执行之前执行一次。 - Andreas Fester
@Andreas:好的,谢谢你(在这里和你的回答中)。我已经包含了规范的那部分并强调了重要的部分。 - Jon Skeet
你好Jon, 非常感谢。你真是个热心肠! 递归过程让我有点困惑...但没关系。 顺便问一下,你用什么工具来反编译字节码? - Wuaner

0

初始化块在实例化类对象时只运行一次(使用任何构造函数 <= 这是您的疑问)。静态初始化块在类被类加载器加载时只运行一次。

这是否意味着在子类的构造函数中使用 this() 将隐式删除超类的无参构造函数的默认调用?

不是。this() 将调用同一类的另一个构造函数,在这种情况下是默认构造函数(如果存在)。此构造函数(默认构造函数)将调用 super()。


代码没有显示任何静态初始化程序。它显示了一个实例初始化程序,该程序将在每个构造的对象上运行一次。 - Jon Skeet
@JonSkeet:我确实错误地阅读了OP代码,但我刚刚补充了我的答案,说了一些关于静态块的内容。 - davidbuzatto
不是很清楚,确切地说。如果你的第二句话是关于实例初始化器的,那么第三句话就会让问题变得混乱无关。我撤销了踩的操作,因为仔细阅读你的答案后,我认为它只是解释得不够清楚(在我看来),而不是完全没有用处。 - Jon Skeet
在我看来,将静态初始化程序加入其中只会增加混乱。 - Jon Skeet
此外,我认为原帖并没有“误解”句子的含义——该句子明显是错误的。实例初始化程序并不会被编译进每个构造函数中。我将编辑我的答案以展示这一点。 - Jon Skeet

0

你错过了第二步,它指出:“如果这个构造函数以对同一类中的另一个构造函数(使用 this)的显式构造函数调用 (§8.8.7.1) 开始,则使用相同的五个步骤递归地评估参数并处理该构造函数调用。” “递归”部分意味着对于“嵌套”执行会执行多次。规范在这里写得不好。 - Jon Skeet
是的,但步骤2也说“继续执行第5步”- 我同意规范写得很糟糕/难以理解.... - Andreas Fester
1
哦,我错过了那一部分。这使得一切更加清晰 :) - Jon Skeet

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