跳到内容

第 1 卷 / 第 7 章 / 第 3 课

7.3 构造方法与对象不变量 ​

前置知识:类、字段、方法和 this

预计时间:30 分钟

完成标志:能用构造方法建立合法对象,并能解释默认构造、重载、链式调用和初始化顺序

1. 构造方法与类同名,没有返回类型 ​

java
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. “默认构造”只在没有声明任何构造方法时出现 ​

java
class Draft {
    String title;
}

编译器会为 Draft 提供一个默认无参构造。若类声明了任何构造方法,编译器就不再补这个构造:

java
class Book {
    Book(String title) { /* ... */ }
}

// new Book(); // 编译错误:没有适用的无参构造

“默认构造方法”专指编译器隐式提供的那个。自己写的 Book() {} 是显式无参构造,不应混称为默认构造。

3. 重载和 this(...) 集中初始化逻辑 ​

java
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. 初始化有顺序 ​

创建普通子类对象时,可以用下面的概念顺序排查问题:

  1. 为新对象的实例字段赋默认值。
  2. 沿 this(...) 委托链找到调用 super(...) 的构造方法,先执行父类构造链。
  3. 按源代码顺序执行当前类的实例字段初始化器和实例初始化块。
  4. 执行直接调用父类构造的那个构造方法的剩余语句;若有 this(...) 委托,再逐层返回并执行各层剩余语句。

构造链细节受类层次和显式构造调用影响。初学阶段最稳妥的做法是把共享校验集中在一个主构造方法中,避免用多个初始化块制造隐式顺序依赖。

5. 构造失败不会返回一个可用对象 ​

若校验抛异常,new Book(...) 表达式不会正常返回引用:

java
try {
    Book invalid = new Book(" ", "某作者", 10.0);
} catch (IllegalArgumentException error) {
    System.out.println(error.getMessage());
}

不要在构造方法完成前把 this 发布到全局集合、回调或其他线程,否则外部可能观察到尚未完成初始化的对象。

也不要从构造方法调用可被子类重写的实例方法。动态分派可能进入子类实现,而子类字段还未完成初始化。实验课会实际复现这个故障。

6. 构造方法与工厂方法各有用途 ​

构造方法直接、可被 new 调用。静态工厂方法可以取有意义的名字、返回缓存实例或返回声明类型的子类型:

java
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:新对象的创建与初始化。

Built with VitePress | Software Systems Atlas