7.3 构造方法与对象不变量
前置知识:类、字段、方法和
this预计时间:30 分钟
完成标志:能用构造方法建立合法对象,并能解释默认构造、重载、链式调用和初始化顺序
1. 构造方法与类同名,没有返回类型
import java.util.Objects;
class Book {
private final String title;
private final String author;
private final double price;
Book(String title, String author, double price) {
this.title = requireText(title, "title");
this.author = requireText(author, "author");
if (!Double.isFinite(price) || price < 0.0) {
throw new IllegalArgumentException("invalid price: " + price);
}
this.price = price;
}
private static String requireText(String value, String name) {
Objects.requireNonNull(value, name);
if (value.isBlank()) {
throw new IllegalArgumentException("blank " + name);
}
return value;
}
}构造方法不是带特殊名字的普通方法:它没有返回类型,不能被对象再次调用,也不参与继承。new Book(...) 选择一个可访问且参数匹配的构造方法。
final 实例字段必须在每条正常完成的构造路径上赋值一次。之后不能让字段改指向其他值,这适合对象身份信息和不可变状态。
示例用 double 是为了衔接基础类型,但二进制浮点不能精确表示许多十进制金额。真实价格通常使用最小货币单位整数或带明确舍入规则的 BigDecimal。
2. “默认构造”只在没有声明任何构造方法时出现
class Draft {
String title;
}编译器会为 Draft 提供一个默认无参构造。若类声明了任何构造方法,编译器就不再补这个构造:
class Book {
Book(String title) { /* ... */ }
}
// new Book(); // 编译错误:没有适用的无参构造“默认构造方法”专指编译器隐式提供的那个。自己写的 Book() {} 是显式无参构造,不应混称为默认构造。
3. 重载和 this(...) 集中初始化逻辑
class Book {
private final String title;
private final String author;
private final double price;
Book(String title, String author) {
this(title, author, 0.0);
}
Book(String title, String author, double price) {
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("blank title");
}
if (author == null || author.isBlank()) {
throw new IllegalArgumentException("blank author");
}
if (!Double.isFinite(price) || price < 0.0) {
throw new IllegalArgumentException("invalid price");
}
this.title = title;
this.author = author;
this.price = price;
}
}多个构造方法按参数列表重载。this(...) 调用同类另一个构造方法,在 Java 17 中必须是构造体的第一条语句,而且构造调用链不能形成循环。
若构造体没有显式写 this(...) 或 super(...),编译器会尝试插入无参 super()。因此父类没有可访问无参构造时,子类必须显式选择其他父类构造。继承章节会继续展开。
4. 初始化有顺序
创建普通子类对象时,可以用下面的概念顺序排查问题:
- 为新对象的实例字段赋默认值。
- 沿
this(...)委托链找到调用super(...)的构造方法,先执行父类构造链。 - 按源代码顺序执行当前类的实例字段初始化器和实例初始化块。
- 执行直接调用父类构造的那个构造方法的剩余语句;若有
this(...)委托,再逐层返回并执行各层剩余语句。
构造链细节受类层次和显式构造调用影响。初学阶段最稳妥的做法是把共享校验集中在一个主构造方法中,避免用多个初始化块制造隐式顺序依赖。
5. 构造失败不会返回一个可用对象
若校验抛异常,new Book(...) 表达式不会正常返回引用:
try {
Book invalid = new Book(" ", "某作者", 10.0);
} catch (IllegalArgumentException error) {
System.out.println(error.getMessage());
}不要在构造方法完成前把 this 发布到全局集合、回调或其他线程,否则外部可能观察到尚未完成初始化的对象。
也不要从构造方法调用可被子类重写的实例方法。动态分派可能进入子类实现,而子类字段还未完成初始化。实验课会实际复现这个故障。
6. 构造方法与工厂方法各有用途
构造方法直接、可被 new 调用。静态工厂方法可以取有意义的名字、返回缓存实例或返回声明类型的子类型:
static Book freeSample(String title, String author) {
return new Book(title, author, 0.0);
}不要为了“设计模式”把每个简单构造都包一层工厂。只有命名、缓存、返回类型或创建流程确实需要时再采用。
专业速查
| 需求 | 做法 |
|---|---|
| 强制必要字段 | 只提供带参构造 |
| 保留无参创建 | 显式声明无参构造 |
| 复用初始化 | this(...) 链到主构造 |
| 拒绝非法状态 | 校验后赋 final 字段 |
| 多种有名称的创建方式 | 静态工厂方法 |
| 精确金额 | long 最小单位或 BigDecimal |
离开这一页前
分别尝试空白书名、null 作者、负价格、NaN 与无穷价格,确认失败构造不会正常返回引用。再解释默认构造与显式无参构造的区别、this(...) 为什么必须先执行,以及构造期间发布 this 或调用可重写方法会暴露什么状态。
规范入口:JLS §12.5:新对象的创建与初始化。