JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · LANGUAGE · JEP 513

Flexible Constructor Bodies:把校验放到 super() 之前

JDK 25 · 速查主题 05 / 06 · 预计 25 分钟
核心25 分钟JEP 513
一句话JDK 25 正式允许在 super(...) / this(...) 之前写语句:可以先校验参数、计算参数、初始化本类字段,再调用父构造器。代价是这些语句处于「早期构造上下文」,不能读取正在构造的对象。

学完你要能自己回答

  1. super() 之前可以写哪些语句?哪些操作会被编译器拒绝?
  2. 为什么允许给字段赋值,却不允许读字段?字段有初始化器 时为什么不行?
  3. prologue 与 epilogue 的执行顺序是什么?这对父构造器调用可覆写方法有什么意义?

旧写法:校验只能塞进 super() 参数01

在 JDK 25 之前,构造器体必须以 super(...) 或 this(...) 开头。想在调用父构造器前校验参数,只能把逻辑塞进参数表达式或静态辅助方法,失败时的堆栈和可读性都不理想。

// 旧写法:校验藏在参数里
Employee(int age, String officeId) {
    super(validateAge(age), officeId);
    this.officeId = officeId;
}

static int validateAge(int age) {
    if (age < 18 || age > 67) {
        throw new IllegalArgumentException("age out of range");
    }
    return age;
}

JEP 513 把这部分语句称为构造器的 prologue:它在显式构造器调用之前执行,可以 fail fast,也可以先初始化本类字段。显式调用之后的代码是 epilogue。

// JDK 25:校验直接写在 super() 之前
Employee(int age, String officeId) {
    if (age < 18 || age > 67) {
        throw new IllegalArgumentException("age out of range");
    }
    super(age);
    this.officeId = officeId;
}
  • prologue 可以包含局部变量、条件、try/catch、throw 等不依赖当前实例的语句。
  • super(...) / this(...) 仍然必须恰好出现一次。
  • prologue 执行完才调用父构造器;父构造器返回后才进入 epilogue。
这不是「允许在 super 前随便用 this」。早期构造上下文的限制很严格,下一节逐条列。

早期构造上下文:能用什么,不能用什么02

prologue 和显式构造器调用的参数列表都属于早期构造上下文(early construction context)。代码不能使用正在构造的实例,只能使用参数、局部变量、静态成员,以及给本类未初始化的字段赋值。

操作允许?说明
使用构造器参数、局部变量允许普通表达式
调用静态方法 / 访问静态字段允许不依赖实例
给本类无初始化器 的字段赋值允许simple assignment;含 final 字段一次赋值
给有初始化器 的字段赋值禁止编译器报 cannot assign initialized field
读取本类字段禁止隐式或显式 this 都不行
调用本类实例方法 / 使用 this禁止不能把 this 传出去
在 prologue 里 return禁止epilogue 可以 return;(不带值)
throw允许fail fast 的典型用法
class Base {
    Base(int x) {}
}

public class BeforeSuperRead extends Base {
    private int value;

    BeforeSuperRead(int x) {
        System.out.println(value); // 读字段
        super(x);
    }
}
BeforeSuperRead.java:9: error: cannot reference value before supertype constructor has been called
        System.out.println(value);
                           ^
1 error
class Base3 {
    Base3(int x) {}
}

public class BeforeSuperThis extends Base3 {
    BeforeSuperThis() {
        super(1);
    }

    BeforeSuperThis(int x) {
        consume(this); // 把 this 传出去
        super(x);
    }

    static void consume(BeforeSuperThis b) {}
}
BeforeSuperThis.java:11: error: cannot reference this before supertype constructor has been called
        consume(this);
                ^
1 error
限制的目的是保证「对象还没有完成构造」这个事实不会被代码忽略。父构造器可能调用可覆写方法,此时子类字段若已初始化,也必须是通过 prologue 合法写入的。

在 super() 前初始化字段03

JEP 513 的另一个目标是把「字段初始化」提前:父构造器可能调用被子类覆写的方法,如果子类字段还没赋值,覆写方法会看到默认值。prologue 允许先给无初始化器 的字段赋值,再调用 super。

class Parent {
    Parent() {
        System.out.println("Parent body");
        hook();
    }

    void hook() {
        System.out.println("Parent hook");
    }
}

public class PrologueOrder extends Parent {
    private final String state;

    PrologueOrder() {
        state = "ready";
        System.out.println("Child prologue");
        super();
        System.out.println("Child epilogue, state=" + state);
    }

    @Override
    void hook() {
        System.out.println("Child hook, state=" + state);
    }

    public static void main(String[] args) {
        new PrologueOrder();
    }
}
Child prologue
Parent body
Child hook, state=ready
Child epilogue, state=ready

输出验证了两件事:子类 prologue 先于父构造器体执行;父构造器调用覆写方法时,state 已经是 ready。final 字段只要没有初始化器,也可以在 prologue 中赋值一次。

  • 只能给本类声明的字段赋值;不能给父类字段赋值。
  • 字段不能有初始化器;private int value = 7; 再在 prologue 赋值会编译失败。
  • prologue 里的赋值是 single assignment 规则的一部分;不要重复赋值 final 字段。
  • 不要在 prologue 里启动线程或注册回调,避免 this 逃逸。
这条能力是为「父构造器调用可覆写方法」的安全性设计的,不是鼓励在构造器里做复杂业务。复杂初始化仍然应该放进工厂方法或依赖注入。

this(...) 之前也可以校验04

this(...) 与 super(...) 一样可以出现在 prologue 之后。常见用法是重载构造器先做统一校验,再委托给主构造器,避免每个重载都重复检查。

class Base5 {
    Base5(int x) {
        System.out.println("Base5(" + x + ")");
    }
}

public class ThisDelegation extends Base5 {
    private final int value;

    ThisDelegation(int x) {
        if (x < 0) {
            throw new IllegalArgumentException("negative");
        }
        this(x, 1);
    }

    ThisDelegation(int x, int factor) {
        super(x * factor);
        this.value = x * factor;
    }

    public static void main(String[] args) {
        System.out.println("value=" + new ThisDelegation(5).value);
        try {
            new ThisDelegation(-1);
        } catch (IllegalArgumentException e) {
            System.out.println("rejected=" + e.getMessage());
        }
    }
}
Base5(5)
value=5
rejected=negative
  • 校验放在委托构造器的 prologue,主构造器只负责赋值。
  • this(...) 和 super(...) 不能同时出现在一个构造器体里。
  • prologue 不能读取主构造器将要设置的字段;它只能使用参数和局部变量。
  • 如果校验依赖父类状态,必须等 super 返回后在 epilogue 做。
重载链越深,prologue 越容易重复。把纯参数校验抽成静态方法仍然可以;灵活构造器体的价值是让失败发生在 super 之前且代码在构造器里可见。

常见坑与边界05

  • 给有初始化器 的字段赋值:编译错误,不是运行期问题。
  • 读字段 / 调实例方法:即使方法不访问字段,也不允许;编译器按语法上下文拒绝。
  • 把 this 传给 lambda 或方法:同样属于引用当前实例,编译失败。
  • 在 prologue 里 return:不允许;epilogue 里可以 return;,但不能 return value;。
  • 以为父类字段已初始化:prologue 阶段父构造器还没运行,父类字段仍是默认值。
  • record 的紧凑构造器:规则不同,不要直接把普通类的 prologue 经验套到 record。
class Base4 {
    Base4() {}
}

public class BeforeSuperInitialized extends Base4 {
    private int value = 7;

    BeforeSuperInitialized() {
        value = 9; // 字段有初始化器
        super();
    }
}
BeforeSuperInitialized.java:9: error: cannot assign initialized field 'value' before supertype constructor has been called
        value = 9;
        ^
1 error
旧写法JDK 25 写法注意
super(validate(x))prologue 里 if + throw失败发生在 super 之前
静态工厂做参数校验构造器 prologue异常类型更直接
字段只能在 super 后赋值无初始化器 字段可提前赋值有初始化器 的字段不行
重载各自校验this(...) 前统一校验避免重复逻辑
JEP 513 在 22–24 预览,25 正式。预览期编译的 class 仍带 65535 minor,不能跨版本运行;升级后要重新编译。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
灵活构造器体22–24 预览 / 25 正式JEP 513;25 起无需 --enable-preview。
super()/this() 前的语句25 允许prologue 不能引用正在构造的实例。
给无初始化器 字段赋值25 允许含 final 字段的一次赋值。
给有初始化器 字段赋值25 禁止cannot assign initialized field。
prologue 中 return25 禁止epilogue 可以 return;。
record 紧凑构造器规则不同不要把普通类 prologue 直接套用。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. super() 之前可以写哪些语句?
可以使用参数、局部变量、静态成员,做条件判断、抛异常、try/catch,并给本类没有初始化器 的字段赋值;不能读取本类字段、调用实例方法、使用 this,也不能在 prologue 里 return。
02. 为什么允许给字段赋值,却不允许读字段?
赋值是为了在父构造器调用可覆写方法前建立子类的不变量;读取则意味着依赖一个尚未完成构造的对象状态,容易看到默认值或半初始化状态,所以被禁止。
03. prologue 与 epilogue 的执行顺序是什么?
子类 prologue 先执行,然后调用父构造器(父类 prologue/构造器体/子类 epilogue 依次展开),super 返回后执行子类 epilogue。整体是 prologue 自下而上、epilogue 自上而下。
04. 给有初始化器 的字段在 super() 前赋值会怎样?
编译失败:cannot assign initialized field ... before supertype constructor has been called。初始化器 已经负责该字段,prologue 不能再写。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。