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)
原因很简单:成员变量未被初始化,name
和 opened
为 null
。
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(...)
调用父类构造器; this
或super
表达式必须是构造器中的第一条语句。
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 项目地址。