1. 构造器是面向对象设计的入口

在 Java 中,构造器(Constructor)扮演着非常关键的角色。它是对象创建过程中初始化内部状态的唯一入口,确保对象从“出生”那一刻起就处于一个合法、可用的状态。

本文将带你深入理解构造器的使用方式,并通过实际代码演示几种常见的构造器模式。

2. 定义一个银行账户类

我们以一个简单的银行账户类为例,它包含如下字段:

  • name:账户名
  • opened:开户时间
  • balance:余额

同时我们重写了 toString() 方法以便打印信息:

class BankAccount {
    String name;
    LocalDateTime opened;
    double balance;

    @Override
    public String toString() {
        return String.format("%s, %s, %f", 
          this.name, this.opened.toString(), this.balance);
    }
}

此时,虽然类定义完整,但没有显式声明构造器。如果直接创建对象并调用 toString(),会抛出空指针异常:

BankAccount account = new BankAccount();
account.toString();

异常栈如下:

java.lang.NullPointerException
    at com.baeldung.constructors.BankAccount.toString(BankAccount.java:12)
    at com.baeldung.constructors.ConstructorUnitTest
      .givenNoExplicitContructor_whenUsed_thenFails(ConstructorUnitTest.java:23)

原因很简单:成员变量未被初始化,nameopenednull

3. 无参构造器

解决这个问题最简单的方式就是添加一个无参构造器:

class BankAccount {
    public BankAccount() {
        this.name = "";
        this.opened = LocalDateTime.now();
        this.balance = 0.0d;
    }
}

✅ 注意事项:

  • 构造器没有返回值类型,甚至连 void 都不能写。
  • 构造器的名称必须与类名完全一致。
  • 如果你不写任何构造器,编译器会自动为你生成一个默认的无参构造器。
  • 默认构造器会将所有字段初始化为其类型的默认值(如对象为 null,数值为 0)。

⚠️ 但这个默认行为并不总是你想要的,特别是当你需要确保对象创建时必须具备某些初始状态时。

4. 带参构造器(Parameterized Constructor)

构造器的真正威力在于它可以在创建对象的同时传入初始状态,从而保证封装性。

我们来定义一个带参构造器,允许在创建账户时传入姓名、开户时间和余额:

class BankAccount {
    public BankAccount() { ... }
    public BankAccount(String name, LocalDateTime opened, double balance) {
        this.name = name;
        this.opened = opened;
        this.balance = balance;
    }
}

现在我们可以这样创建对象:

LocalDateTime opened = LocalDateTime.of(2018, Month.JUNE, 29, 06, 30, 00);
BankAccount account = new BankAccount("Tom", opened, 1000.0f); 
account.toString();

此时类中存在两个构造器:无参构造器和带参构造器。Java 支持多个构造器(构造器重载),但别滥用,否则会增加代码维护成本。

🔧 如果你发现构造器过多,不妨考虑使用 建造者模式(Builder Pattern) 或其他创建型设计模式。

5. 拷贝构造器(Copy Constructor)

构造器不仅用于初始化,还可以用于创建已有对象的副本。

比如我们想从一个已有账户创建一个新账户,新账户继承原账户的姓名,但开户时间是当前时间,余额为 0。这时可以使用拷贝构造器:

public BankAccount(BankAccount other) {
    this.name = other.name;
    this.opened = LocalDateTime.now();
    this.balance = 0.0f;
}

使用示例:

LocalDateTime opened = LocalDateTime.of(2018, Month.JUNE, 29, 06, 30, 00);
BankAccount account = new BankAccount("Tim", opened, 1000.0f);
BankAccount newAccount = new BankAccount(account);

assertThat(account.getName()).isEqualTo(newAccount.getName());
assertThat(account.getOpened()).isNotEqualTo(newAccount.getOpened());
assertThat(newAccount.getBalance()).isEqualTo(0.0f);

✅ 这种方式比 clone() 更直观、安全,是 Java 中常用的替代方案。

6. 构造器链式调用(Chained Constructor)

有时候我们想给某些字段提供默认值,或者从一个构造器调用另一个构造器来复用代码。这时可以使用构造器链。

比如,只传入账户名时,自动使用当前时间和 0 余额:

public BankAccount(String name, LocalDateTime opened, double balance) {
    this.name = name;
    this.opened = opened;
    this.balance = balance;
}

public BankAccount(String name) {
    this(name, LocalDateTime.now(), 0.0f);
}

📌 要点:

  • 使用 this(...) 调用本类的其他构造器;
  • 使用 super(...) 调用父类构造器;
  • thissuper 表达式必须是构造器中的第一条语句

7. 值类型与不可变对象

在 Java 中,构造器也常用于创建值对象(Value Object)。值对象的特点是:一旦创建,其内部状态不可更改(Immutable)

为了实现不可变性,我们通常会使用 final 关键字:

class Transaction {
    final BankAccount bankAccount;
    final LocalDateTime date;
    final double amount;

    public Transaction(BankAccount account, LocalDateTime date, double amount) {
        this.bankAccount = account;
        this.date = date;
        this.amount = amount;
    }
}

final 字段必须在构造器中初始化,且之后不能再被修改。

⚠️ 如果类中有多个构造器,每个构造器都必须对所有 final 字段进行初始化,否则编译器会报错。

8. 总结

构造器是 Java 面向对象编程的核心机制之一,合理使用可以让你的对象从创建之初就具备良好的封装性和一致性。

  • ✅ 无参构造器适合默认初始化;
  • ✅ 带参构造器支持灵活传参;
  • ✅ 拷贝构造器用于对象复制;
  • ✅ 构造器链提高代码复用性;
  • final 字段搭配构造器实现不可变对象;

如需查看完整代码示例,请访问 GitHub 项目地址


原始标题:A Guide to Constructors in Java