Java语法专题1: 类的构造顺序

时间:2024-01-19 19:20:08

问题

下面的第二个问题来源于Oracle的笔试题, 非常经典的一个问题, 我从07年开始用了十几年. 看似简单, 做对的比例不到2/10.

  • 描述一下多级继承中类的构造顺序
  • 给定两段代码, 分别是父类和子类, 写出(或选择)正确的输出

    代码如下
public class Base {
public Base() {
method(100);
}
public void method(int i) {
System.out.println("Base::method " + i);
}
} public class Sub extends Base {
public Sub() {
super.method(70);
}
public void method(int j) {
System.out.println("Sub::method " + j);
}
public void method(String j) {
System.out.println("Sub::passed " + j);
}
public static void main(String[] args) {
Base b1 = new Base();
Base b2 = new Sub();
}
}

分析

这是属于Java中常见的基础概念问题, 正确回答这些问题, 需要对类的这些知识有清晰的了解:

  1. Java类实例的初始化顺序
  2. Java类方法的重写, 以及和重载的区别

以下通过具体场景说明

场景一

先看下面代码, 执行Sub.java时的屏幕输出是什么?

Base.java

public class Base {
public Base() {
method(100);
}
public void method(int i) {
System.out.println("Base::method " + i);
}
}

Sub.java

public class Sub extends Base {
public void method(int j) {
System.out.println("Sub::method " + j);
}
public static void main(String args[]) {
Base b2 = new Sub();
}
}

这里有两个很重要的概念:

  1. 类初始化时隐藏的构造顺序: 先调用父类的默认构造函数(即不带参数的构造函数), 然后再执行当前构造函数, 因为本例中Sub.java的构造函数为空(未定义), 因此实际执行的是父类的默认构造函数
  2. 父类的函数, 会被子类定义的同参数方法覆盖, 这叫方法重写. 本例中初始化的类是Sub, Sub中的method(int i), 已经重新定义, 因此父类的默认构造函数中调用的是Sub的method(int i).

输出

Sub::method 100

场景二

保持父类Base.java不变, 在Sub.java中增加子类的构造函数, 执行Sub.java时的屏幕输出是什么?

public class Sub extends Base {
public Sub() {
super.method(70);
}
public void method(int j) {
System.out.println("Sub::method " + j);
}
public static void main(String args[]) {
Base b2 = new Sub();
}
}

第一行的输出可以参照场景一的说明, 初始化Sub时, 会隐藏调用父类的默认构造函数, 第二行则是子类构造函数中, 在super.method(70);中指定使用父类的method(int i)方法产生的结果.

这个输出验证了前面说的类初始化时的隐藏初始化顺序: 会先调用父类的默认构造函数(即不带参数的构造函数), 然后再执行当前构造函数里的逻辑. 这是最容易出错地方, 漏了第一行, 忘记了即使子类定义了构造函数, 父类构造函数一样会执行.

输出是

Sub::method 100
Base::method 70

场景三

再验证一下类初始化时的隐藏初始化顺序, 如果父类和子类都增加了带变量的构造函数, 执行Sub.java时的屏幕输出是什么?

Base.java

public class Base {
public Base() {
method(100);
}
public Base(int i) {
method(i);
}
public void method(int i) {
System.out.println("Base::method " + i);
}
}

Sub.java

public class Sub extends Base {
public Sub(int i) {
super.method(70);
}
public void method(int j) {
System.out.println("Sub::method " + j);
}
public static void main(String args[]) {
//Base b1 = new Base();
Base b2 = new Sub(100);
}
}

依然先调用了父类的默认构造函数(不带参数), 再调用子类的构造函数, 输出是

Sub::method 100
Base::method 70

场景四

父类不带默认构造函数

Base.java

public class Base {
public Base(int i) {
method(i);
}
public void method(int i) {
System.out.println("Base::method " + i);
}
}

此时Sub.java如果还使用前一个场景的代码, 就会无法通过编译, 因为找不到父类的默认构造函数了, 此时隐藏的初始化顺序就失效了, 需要指定使用哪个父类构造函数

Sub.java

public class Sub extends Base {
public Sub(int i) {
super(100); // <------- 需要显式指定构造函数
super.method(70);
}
public void method(int j) {
System.out.println("Sub::method " + j);
}
public static void main(String args[]) {
//Base b1 = new Base();
Base b2 = new Sub(100);
}
}

执行Sub.java时的屏幕输出依然是

Sub::method 100
Base::method 70

总结

问题: 描述一下多级继承中类的构造顺序

解答:

  1. 类初始化时的隐藏逻辑: 先调用父类的默认构造函数, 再调用子类的默认构造函数
  2. 当父类定义了带参数的构造函数, 又不定义默认构造函数时, 上一条就失效了, 子类必须显式指定父类的构造函数

相关文章