目录
前言
为什么子类会自动调用父类的无参构造函数呢?再回答这个问题之前先来说一下两个常见的概念.
继承与构造函数
继承和构造函数是面向对象编程中两个重要的概念,它们在 Java 中起着至关重要的作用。
继承
继承是面向对象编程中的一个核心概念,它允许一个类(称为子类)继承另一个类(称为父类)的属性和方法。通过继承,子类可以复用父类的代码,并且可以在此基础上扩展和定制功能,从而实现代码的重用和层次化组织。在 Java 中,使用关键字 extends
来声明类之间的继承关系。
继承的特点包括:
- 代码复用性: 子类可以继承父类的属性和方法,避免了重复编写相似的代码。
- 层次化组织: 可以通过继承构建类的层次结构,方便管理和维护代码。
- 多态性支持: 继承是实现多态性的基础,在运行时可以根据实际类型调用相应的方法。
构造函数
构造函数是一种特殊类型的方法,它用于创建对象并对对象进行初始化。在 Java 中,构造函数的名称必须与类名完全相同,不返回任何值,且不能被显式调用,而是在创建对象时自动调用。构造函数的作用是初始化对象的状态,为对象的属性赋初值。
构造函数的特点包括:
- 对象初始化: 构造函数用于初始化对象的状态,为对象的属性赋初值,确保对象在创建后处于合适的状态。
- 与类同名: 构造函数的名称必须与类名相同,这样才能在创建对象时被自动调用。
- 重载: 可以定义多个构造函数,它们可以有不同的参数列表,以满足不同的初始化需求。
继承与构造函数的关系
在 Java 中,当创建子类的实例时,子类的构造函数会隐式地调用父类的构造函数,以确保父类的状态得到正确的初始化。如果子类的构造函数没有显式调用父类的构造函数,编译器会自动插入对父类的无参构造函数的调用。这是为了确保父类在子类实例化时能够得到正确的初始化,也符合面向对象编程中继承的特性,确保子类能够正常地继承父类的属性和方法。
接下来说一下为什么子类会自动调用父类的无参构造函数呢?
默认调用父类无参构造函数的原因
初始化父类状态
在 Java 中,如果子类的构造函数没有显式调用父类的构造函数,编译器会自动插入对父类的无参构造函数的调用。这是为了确保父类在子类实例化时能够得到正确的初始化。因为子类继承了父类的属性和方法,父类的状态需要得到正确的初始化,以便子类能够正常地继承父类的特性。
当一个对象被创建时,它的整个继承体系都需要得到正确的初始化。这包括了父类和所有祖先类的属性、方法等状态。如果子类没有显式地调用父类的构造函数,那么就无法保证父类的状态得到正确地初始化,从而可能导致程序出现意料之外的行为。
通过在子类的构造函数中调用父类的构造函数,可以确保父类的状态得到正确地初始化,为子类提供一个良好的起点。这也符合面向对象编程中的继承机制,即子类继承了父类的属性和方法,并且在创建子类实例时需要保证整个继承链的正确性。
因此,子类要先调用父类的构造函数,以确保整个继承体系得到正确的初始化,从而遵循面向对象编程的原则。
继承链的完整性
每个类都直接或间接地继承自 java.lang.Object
类,Object
类是所有类的顶级父类,即使没有显式指定父类,也会默认继承自 Object
类,而Object
类有一个无参的构造函数。
当创建一个子类的实例时,如果子类的构造函数没有显式地调用父类的构造函数,编译器会自动在子类的构造函数中插入对父类构造函数的调用。这就是所谓的隐式的 super()
调用。super()
可以理解为是指向自己超(父)类对象的一个指针,而这个父类指的是离自己最近的一个父类。当子类的构造函数执行时,它的第一行隐含地调用了 super()
,这就是为什么在子类构造函数中并没有显式调用父类构造函数,但父类的构造函数仍然会被调用的原因。
简而言之,隐式的 super()
调用是为了确保整个继承链的正确初始化,使得子类在实例化时能够正常继承父类的属性和方法。
如果父类中没有无参的构造函数,而只有带参数的构造函数,那么子类的构造函数必须显式地调用父类的某个构造函数,否则会出现编译错误。在子类的构造函数中使用super()
关键字可以调用父类的构造函数,通过super(parameter_list)
可以调用父类的带参数的构造函数。
需要注意的是,如果父类没有提供无参构造函数,而子类的构造函数又没有显式地调用父类的某个构造函数,编译器会报错,提示需要在子类的构造函数中显式调用父类的构造函数。
示例代码
当在 Java 中创建子类实例时,如果没有显式调用父类构造函数,会默认调用父类的无参构造函数。下面结合代码讲解一下。
假设有一个父类 Parent
和一个子类 Child
,它们的代码如下所示:
// Parent.java
public class Parent {
public Parent() {
System.out.println("父类构造被调用");
}
}
// Child.java
public class Child extends Parent {
public Child() {
System.out.println("子类构造被调用");
}
}
当创建子类 Child
的实例时,会发生以下情况:
public class Main {
public static void main(String[] args) {
Child child = new Child();
}
}
输出结果为:
父类构造被调用
子类构造被调用
这里没有在Child
类的构造函数中显式调用父类构造函数,但是父类的无参构造函数依然被调用了。这是因为在 Java 中,如果子类的构造函数没有显式调用父类构造函数,编译器会自动插入对父类的无参构造函数的调用。
如果父类没有提供无参构造函数,而只有带参数的构造函数,那么子类的构造函数必须显式地调用父类的某个构造函数。例如,如果Parent
类只有一个带参数的构造函数,那么Child
类的构造函数可以这样写:
public class Child extends Parent {
public Child() {
super("Hello"); // 显式调用父类的带参数构造函数
System.out.println("子类构造被调用");
}
}
在这个例子中,示了子类默认调用父类无参构造函数的行为,并且介绍了在子类中显式调用父类构造函数的方法。
继承后对象初始化顺序
讲到构造函数我想再进一步说一下子类继承父类时,对象的初始化顺序涉及到多个方面,包括静态成员、实例成员,以及构造函数的调用。
-
静态成员初始化: 首先,父类的静态成员变量和静态初始化块会按照其在类中出现的顺序依次执行,然后是子类的静态成员变量和静态初始化块。
-
父类实例成员初始化和构造函数: 接着,父类的实例成员变量会被赋予默认值,然后执行父类的构造函数。如果子类没有显式调用父类构造函数,编译器会自动插入对父类的无参构造函数的调用。
-
子类实例成员初始化和构造函数: 父类的构造函数执行完成后,子类的实例成员变量会被赋予默认值,然后执行子类的构造函数。
图示
示例代码
class Parent {
static {
System.out.println("Parent's static block");
}
{
System.out.println("Parent's instance block");
}
public Parent() {
System.out.println("Parent's constructor");
}
}
class Child extends Parent {
static {
System.out.println("Child's static block");
}
{
System.out.println("Child's instance block");
}
public Child() {
System.out.println("Child's constructor");
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
}
}
运行结果为
Parent's static block
Child's static block
Parent's instance block
Parent's constructor
Child's instance block
Child's constructor
从输出结果可以看出,初始化顺序遵循上述所描述的规则。
总结
在 Java 中,子类默认调用父类的无参构造函数是为了确保整个继承链得到正确的初始化,使得子类能够正常继承父类的属性和方法。这符合面向对象编程中继承的特性,确保子类能够正确地继承父类的状态。