在 Java 中,对象的初始化顺序都是遵循一定的规则的。这涉及到类的构造函数调用、字段初始化以及继承关系中的初始化顺序。
当涉及到继承时,初始化顺序如下:
- 父类静态变量和静态初始化块:按照声明的顺序执行。
- 子类静态变量和静态初始化块:同样按照声明的顺序执行。
- 父类实例变量和实例初始化块:按照声明的顺序执行。
- 父类构造函数:执行父类的构造函数。
- 子类实例变量和实例初始化块:按照声明的顺序执行。
- 子类构造函数:执行子类的构造函数。
注意:在子类的构造函数中,可以通过 super() 显式地调用父类的构造函数(如果没有显式调用,则会自动调用父类的无参构造函数)。这个 super() 的调用必须在子类构造函数的第一行。
关于你提到的“父类的构造函数早于子类的属性初始化”,实际上,这是符合上述规则的。在子类的构造函数被调用之前,父类的构造函数已经执行完毕。这意味着父类中的所有实例变量和实例初始化块都已经被初始化。然后,子类的实例变量和实例初始化块才会被初始化,最后执行子类的构造函数。
这里有一个简单的例子来说明这个过程:
| class Parent {  | |
| int parentField;  | |
| public Parent() {  | |
| System.out.println("Parent Constructor");  | |
| parentField = 10;  | |
| }  | |
| {  | |
| System.out.println("Parent Instance Block");  | |
| }  | |
| }  | |
| class Child extends Parent {  | |
| int childField;  | |
| public Child() {  | |
| super(); // 隐式调用父类构造函数  | |
| System.out.println("Child Constructor");  | |
| childField = 20;  | |
| }  | |
| {  | |
| System.out.println("Child Instance Block");  | |
| }  | |
| }  | |
| public class Main {  | |
| public static void main(String[] args) {  | |
| new Child();  | |
| }  | |
| } | 
输出:
| Parent Instance Block  | |
| Parent Constructor  | |
| Child Instance Block  | |
| Child Constructor | 
从输出中可以看出,父类的实例初始化块和构造函数在子类的任何初始化之前执行。然后,子类的实例初始化块和构造函数才会执行。