1. 概述

在这个教程中,我们将学习Java如何处理构造函数,并参考《Java语言规范》(https://docs.oracle.com/javase/specs/)中与此相关的一些规则。

2. 构造函数声明

在Java中,每个类都必须有一个构造函数。它的结构类似于方法,但其目的不同。

让我们来看看构造函数的定义:

<Constructor Modifiers> <Constructor Declarator> [Throws Clause] <Constructor Body>

接下来逐个分析各个部分。

2.1. 构造函数修饰符

构造函数声明以访问修饰符开头:它们可以是publicprivateprotected或包访问级别,取决于其他访问修饰符。

为了防止编译错误,构造函数声明中不允许有多个privateprotectedpublic修饰符。

与方法不同,构造函数不能是abstractstaticfinalnativesynchronized

  • 由于构造函数不是类成员且不继承,因此不需要声明为final
  • 抽象性是不必要的,因为我们必须实现构造函数。
  • 因为每个构造函数都是通过对象调用的,所以不需要静态构造函数。
  • 在构造过程中,正在构建的对象不应被synchronized,因为这会锁定对象,而通常直到所有构造函数完成工作后,其他线程才能访问它。
  • Java中没有native构造函数,这是语言设计决策的一部分,目的是确保在对象创建时始终调用超类构造函数。

2.2. 构造函数声明器

让我们看看构造函数声明器的语法:

Constrcutor Name (Parameter List)

构造函数声明器中的构造函数名称必须与包含构造函数声明的类的名称匹配,否则会导致编译时错误。

2.3. 异常抛出条款

方法和构造函数的throws条款的结构和行为是相同的。

2.4. 构造函数体

构造函数体的语法如下:

Constructor Body: { [Explicit Constructor Invocation] [Block Statements] }

我们可以在构造函数体的第一条命令中显式调用同一类的另一个构造函数或直接的超类构造函数。同一个构造函数的直接或间接调用是不允许的。

3. 显式构造函数调用

构造函数的调用可以分为两类:

  • 使用this的交替构造函数调用用于调用同一类的其他构造函数。
  • 使用super的超类构造函数调用用于调用直接超类的构造函数。

让我们看一个使用thissuper关键字调用其他构造函数的例子:

class Person {
    String name;

    public Person() {
        this("Arash");   //ExplicitConstructorInvocation
    }

    public Person(String name){
        this.name = name;
    }
}

Employee类的第一个构造函数中,它调用了超类Person的构造函数,并传递了id:

class Person {
    int id;
    public Person(int id) {
        this.id = id;
    }
}

class Employee extends Person {
    String name;
    public Employee(int id) {
        super(id);
    }
    public Employee(int id, String name) {
        super(id);
        this.name = name;
    }
}

4. 构造函数调用规则

4.1. thissuper必须是构造函数中的第一条语句

无论何时调用构造函数,都必须调用其基类的构造函数。此外,你可以在类内部调用另一个构造函数。Java通过要求构造函数的第一条调用是thissuper来强制执行这个规则。

让我们看一个例子:

class Person {
    Person() {
        //
    }
}
class Employee extends Person {
    Employee() {
        // 
    }
}

下面是构造函数编译的一个示例:

.class Employee
.super Person
; A constructor taking no arguments
.method <init>()V
aload_0
invokespecial Person/<init>()V
return
.end method

构造函数的编译类似编译其他方法,只是生成的方法名称为<init>。验证<init>方法的要求之一是必须首先调用超类构造函数(或当前类中的其他构造函数)。

如上所述,Person类必须调用其超类构造函数,以此类推,直到java.lang.Object

当类必须调用其超类构造函数时,这确保了它们永远不会在未正确初始化的情况下使用。JVM的安全性依赖于此,因为有些方法在类初始化之前将无法工作。

4.2. 构造函数中不要同时使用thissuper

想象一下,如果我们在构造函数体中同时使用thissuper会发生什么。

让我们通过一个例子来看:

class Person {
    String name;
    public Person() {
        this("Arash");
    }

    public Person(String name) {
        this.name = name;
    }
}

class Employee extends Person {
    int id;
    public Employee() {
        super();
    }

    public Employee(String name) {
        super(name);
    }

    public Employee(int id) {
        this();
        super("John"); // syntax error
        this.id = id;
    }

    public static void main(String[] args) {
        new Employee(100);
    }
}

我们不能执行上述代码,因为会出现编译时错误。当然,Java编译器有其合理的解释。

让我们看看构造函数调用序列:

constructor-2

Java编译器不允许编译此程序,因为初始化不清楚。

4.3. 递归构造函数调用

如果构造函数调用自身,编译器会报错。例如,在以下Java代码中,由于试图在构造函数内部调用同一个构造函数,编译器会报错:

public class RecursiveConstructorInvocation {
    public RecursiveConstructorInvocation() {
        this();
    }
}

尽管Java编译器的限制,我们可以通过稍微修改代码来编译程序,但这会导致栈溢出:

public class RecursiveConstructorInvocation {
    public RecursiveConstructorInvocation() {
        RecursiveConstructorInvocation rci = new RecursiveConstructorInvocation();
    }

    public static void main(String[] args) {
        new RecursiveConstructorInvocation();
    }
}

我们创建了一个RecursiveConstructorInvocation对象,通过调用构造函数进行初始化。然后构造函数创建另一个RecursiveConstructorInvocation对象,再次调用构造函数,直到栈溢出。

现在,让我们看看输出:

Exception in thread "main" java.lang.StackOverflowError
    at org.example.RecursiveConstructorInvocation.<init>(RecursiveConstructorInvocation.java:29)
    at org.example.RecursiveConstructorInvocation.<init>(RecursiveConstructorInvocation.java:29)
    at org.example.RecursiveConstructorInvocation.<init>(RecursiveConstructorInvocation.java:29)
//...

5. 总结

在这篇教程中,我们讨论了Java中构造函数的规范,并复习了一些理解类和超类构造函数调用的规则。

一如既往,代码示例可以在GitHub上找到。