Java中构造函数和继承机制结合使用时,容易出现不少隐蔽的逻辑问题,其中构造函数无限循环是最容易让开发者踩坑的情况之一,这类问题往往在程序运行时才会暴露,排查起来也有一定难度。

Java构造函数与继承的基础规则
在Java中,子类继承父类时,子类构造函数的第一行默认会隐式调用父类的无参构造函数,如果父类没有无参构造函数,子类就必须显式通过super()调用父类的有参构造函数,否则代码会编译失败。同时,构造函数本身不会被子继承,子类只能调用父类的构造函数,而不能重写父类的构造函数。
我们可以通过一段基础代码来理解这个规则:
// 父类定义
class Parent {
private String name;
// 父类有参构造函数
public Parent(String name) {
this.name = name;
System.out.println("父类构造函数执行,name:" + name);
}
}
// 子类定义
class Child extends Parent {
private int age;
// 子类构造函数,显式调用父类有参构造函数
public Child(String name, int age) {
super(name); // 必须放在第一行,否则编译报错
this.age = age;
System.out.println("子类构造函数执行,age:" + age);
}
}
public class Test {
public static void main(String[] args) {
Child child = new Child("张三", 10);
}
}构造函数无限循环的常见场景
构造函数无限循环通常出现在父类构造函数和子类构造函数的调用逻辑形成闭环的场景,最常见的有两种情况。
场景一:父类构造函数调用子类重写的方法
如果父类构造函数中调用了被子类重写的方法,而这个方法又触发了子类的实例化,就会形成循环。比如下面的代码:
class Parent {
public Parent() {
// 父类构造函数调用被子类重写的方法
init();
}
public void init() {
System.out.println("父类init方法");
}
}
class Child extends Parent {
private static Child instance;
public Child() {
// 子类构造函数中尝试创建自身实例
instance = new Child();
}
@Override
public void init() {
// 父类构造函数调用init时,实际执行子类重写的逻辑,触发子类实例化
new Child();
}
}
public class TestLoop {
public static void main(String[] args) {
// 创建子类实例时触发无限循环
Child child = new Child();
}
}这段代码中,创建Child实例时,先调用父类Parent的无参构造函数,父类构造函数调用init()方法,由于多态特性,实际执行的是子类重写的init()方法,该方法又会创建新的Child实例,新的实例又会重复上述流程,最终形成无限循环,抛出栈溢出错误。
场景二:构造函数中互相调用形成闭环
如果子类构造函数显式调用this()触发自身其他构造函数,而其他构造函数又错误调用了会触发父类实例化的逻辑,也可能形成循环。比如:
class Parent {
public Parent() {
// 父类构造函数中创建子类实例
new Child();
}
}
class Child extends Parent {
public Child() {
// 调用自身其他构造函数
this("默认名称");
}
public Child(String name) {
// 隐式调用父类无参构造函数
}
}
public class TestLoop2 {
public static void main(String[] args) {
Child child = new Child();
}
}这段代码的执行流程是:创建Child实例,调用子类无参构造函数,无参构造函数调用this("默认名称"),进入有参构造函数,有参构造函数隐式调用父类无参构造函数,父类构造函数中又创建新的Child实例,新的实例重复上述流程,最终也会形成无限循环。
避免无限循环的解决方案
针对上述问题,我们可以遵循以下实践来规避构造函数无限循环的问题:
- 尽量避免在父类构造函数中调用可被重写的方法,如果必须调用,可以将方法声明为
final,避免子类重写,或者声明为private,确保不会被外部访问。 - 构造函数中不要编写过于复杂的逻辑,尤其是不要触发当前类或子类的实例化操作,构造函数只负责初始化当前对象的成员变量即可。
- 如果父类需要初始化一些通用逻辑,可以将逻辑抽成独立的初始化方法,在对象创建完成后由外部调用,而不是放在构造函数中执行。
- 子类重写父类方法时,不要在方法中触发当前类或父类的实例化操作,避免形成调用闭环。
最佳实践总结
编写涉及继承的构造函数时,首先要明确构造函数的职责只是初始化当前对象的属性,不要承载额外的业务逻辑。其次要牢记Java构造函数的调用链规则,子类构造函数一定会先调用父类构造函数,父类构造函数的执行时机早于子类构造函数。最后在编码时尽量避免在构造函数中调用可重写方法、触发其他类实例化,就能有效规避无限循环这类陷阱,写出更稳定的继承结构代码。