今天在看代码的时候,发现了一段奇怪的构造器代码——构造器里面居然有一行
this(...),后面啥也没有了。第一反应是:“这个this怎么长得跟super这么像?难道作用是一样的?”后来查了资料才知道,原来 Java 里的
this除了可以指向当前实例对象,还能作为构造器委托使用。这篇文章就把this(...)相关的所有知识点一次性讲清楚。
一、从一段代码说起
先来看我遇到的那段代码:
publicclassLotXXXXXXcationDomainService{// 四参构造器(public,对外暴露)publicLotXXXXXXationDomainService(XXXXXXcationQueryGatewayqueryGateway,XXXXXXationCommandGatewaycommandGateway,XXXXCredentialGatewaycredentialGateway,XXXXXXcationQueryGatewayXXXXXcationQueryGateway){// 注意这一行:this(...) 直接调用五参构造器,把真实时钟作为默认值传过去this(queryGateway,commandGateway,credentialGateway,XXXXXcationQueryGateway,System::currentTimeMillis);}// 五参构造器(包级可见,无修饰符)LotXXXXXXXXionDomainService(XXXXXXcationQueryGatewayqueryGateway,XXXXXXationCommandGatewaycommandGateway,XXXXCredentialGatewaycredentialGateway,XXXXXXcationQueryGatewayXXXXXcationQueryGateway,LongSuppliercurrentTimeMillis){this.queryGateway=queryGateway;this.commandGateway=commandGateway;this.credentialGateway=credentialGateway;this.XXXXXcationQueryGateway=XXXXXcationQueryGateway;this.currentTimeMillis=currentTimeMillis;}}问题来了:四参构造器里面的this(...)是什么?它跟super(...)是同一回事吗?
答案:不是一回事,但有点类似。super(...)是调用父类的构造器,而this(...)是调用同一个类的另一个构造器。这种写法就叫构造器委托(constructor chaining / constructor delegation)。
构造器委托,顾名思义,就是让一个构造器去委托另一个构造器来完成实际的初始化工作。通常用于:用户只需要传入几个关键参数,系统内部自动补充一些额外的参数(比如 UUID、创建时间、或者像这里的时钟源)。
二、语法本质:this(…) 是什么
this(参数...)是一条只能在构造器里使用的特殊语句,作用是调用同一个类的另一个构造器。
publicclassA{publicA(){this(42);// 调用 A(int) 构造器}publicA(intx){// 被"链"到的构造器,真正干活的地方// ...}}注意:this(...)不是方法调用。它调用的目标是构造器,并且只能是自己类的构造器(想调父类的用super(...))。
三、执行顺序
以文章开头的代码为例:
newLotXXXXXXcationDomainService(4个网关参数)→ 进入四参构造器 → 第一句this(...)跳到五参构造器 → 五参构造器把5个字段全部赋值,返回 → 回到四参构造器(后面没代码了),构造完成四个字段只在五参构造器里赋值一次。四参构造器自己一行赋值都没有——它的唯一职责就是"补上默认时钟参数,然后转发给五参构造器"。
四、为什么这样写?
反例:假如不用委托,两个构造器各写各的
// 反例:字段赋值代码重复publicLotXXXXXXXXionDomainService(网关×4){this.queryGateway=queryGateway;this.commandGateway=commandGateway;this.credentialGateway=credentialGateway;this.XXXXXXcationQueryGateway=XXXXXXcationQueryGateway;this.currentTimeMillis=System::currentTimeMillis;// 与下面重复}LotXXXXXXXXionDomainService(网关×4,LongSuppliercurrentTimeMillis){this.queryGateway=queryGateway;// 重复this.commandGateway=commandGateway;// 重复// ... 全部重复一遍this.currentTimeMillis=currentTimeMillis;}问题很明显:
- 5 行赋值重复了两遍。以后加一个字段,要改两个构造器,漏改一个就会出现"某条构造路径字段为 null"的隐蔽 bug
- 默认值
System::currentTimeMillis散落在各处,不好统一管理
委托写法的好处
- 赋值只有一个地方:五参构造器是唯一"干活"的构造器
- 四参构造器只是入口:它的存在意义就是"时钟参数默认为系统时钟"
- 新增字段只改五参构造器,两个入口自动同步
这其实就是 Java 版的**"默认参数"惯用法**。很多语言原生就支持默认参数,比如 Python:
def__init__(self,queryGateway,...,currentTimeMillis=System.currentTimeMillis):# ...Java 没有默认参数语法,就用"多个构造器 + 委托链"来模拟:参数少的构造器提供默认值,然后转发给参数全的构造器。
五、完整知识点清单
知识点①:必须是构造器的第一条语句
publicA(){intx=1;// 编译不通过,this(...) 之前不能有任何语句this(42);}原因:对象必须先完整初始化(跑完整个构造链),才能执行任何其他逻辑。否则构造链后面的代码用到的变量可能还没有完成初始化。
唯一的例外形式(Java 允许):
publicA(){this(computeDefault());//正常,参数表达式里可以调用静态方法}语句本身还是第一条,computeDefault()在这里时作为方法的“参数"而不是"执行语句"。
知识点②:this(…) 和 super(…) 互斥
在同一个构造器里,this(...)和super(...)只能出现其中一个(而且都只能放在第一条语句的位置):
publicA(){super();// 编译错误:this 和 super 不能共存this(42);}但委托链的不同层可以各自使用:A()委托给A(int),A(int)里调用super(...),这是完全合法的标准写法。
知识点③:不能成环
publicA(){this(1);}// 编译错误:A() → A(int) → A() 循环publicA(intx){this();}编译器会做静态分析,直接拒绝自委托(this()调自己)和间接环。所以构造链必然是一棵有向无环图,最末端那个构造器要么显式调super(...),要么隐式调super()。
知识点④:隐式 super() 的存在
如果构造器里既不写this(...)也不写super(...),编译器会自动在开头插入无参super()。
所以任何构造链最终一定落到某个显式或隐式的super(...),保证父类先初始化——父类构造先于子类构造体执行,这个顺序不可以绕过。
知识点⑤:完整的执行顺序模型
classBase{Base(){System.out.print("Base ");}}classAextendsBase{A(){this(1);System.out.print("A() ");}A(intx){System.out.print("A(int) ");// 最底层,隐式 super()}}newA();// 输出:Base A(int) A()记忆:委托链先深入到底,再逐层往回执行。(类似递归dp)
知识点⑥:字段初始化的时机(经典的陷阱)
字段初始化和实例初始化块({ ... })在构造链回程中执行。具体来说:super()返回后、本构造器体执行前,执行本构造器所属层的字段初始化。
这个细节引出了一个经典陷阱:
classA{privateintx=init();// 字段初始化A(){this(1);// 先跳去 A(int)}A(intx){useX();// 此时 A() 层的字段初始化还没执行!}}如果useX()依赖x,而x的初始化写在A()这个委托层——被委托的A(int)里的x还是默认值(0),还没被初始化。
规则:字段初始化写在哪一层,就在那一层的构造器体之前执行;委托会跳过委托层的字段初始化。
知识点⑦:this(…) 里的 this 与 this.xxx 里的 this 是两回事
| 写法 | 含义 | 本质 |
|---|---|---|
this.field/this.method() | 指向当前实例的引用 | 实例引用 |
this(...) | 调用本类的另一个构造器 | 构造器委托语法 |
判别方法:this后面跟括号就是委托,跟点就是实例引用。同一个关键字,两种语法形态,含义完全不同——可以理解为语言设计者复用了this这个词,仅此而已。
所以,回到文章开头的问题:this(...)和super(...)是同一回事吗?
不是。super(...)是调用父类构造器(向上走),this(...)是调用本类的另一个构造器(平级跳转)。它们只是长得像,作用对象完全不同。
六、典型用途
用途①:模拟默认参数(本项目的用法)
// 公共入口:给时钟参数填上默认值(系统时钟)publicLotXXXXXXXXionDomainService(网关×4){this(网关×4,System::currentTimeMillis);}// 完整版构造器:真正干活的LotXXXXXXXXionDomainService(网关×4,LongSupplierclock){// ...全部赋值...}用途②:望远镜式构造器(逐层补默认值)
publicA(){this(0,0,"default");}publicA(intx){this(x,0,"default");}publicA(intx,inty){this(x,y,"default");}publicA(intx,inty,Strings){/* 真正干活 */}用途③:统一初始化路径
多个构造器共享一段必须执行的初始化逻辑(校验、资源获取),避免每个构造器重复一遍。
七、什么时候不该用它以及替代方案
超过 3 层委托就该换方案了。望远镜构造器模式本身被《Effective Java》(第 2 条)列为需要警惕的模式。
替代方案 A:静态工厂方法(推荐)
publicstaticLotXXXXXXXXionDomainServicecreate(网关×4){returnnewLotXXXXXXXXionDomainService(网关×4,System::currentTimeMillis);}工厂方法的优势:有名字、可以返回缓存/子类、不用暴露构造器。
替代方案 B:Builder 模式
参数多、可选组合多的时候使用:
A.builder().x(1).y(2).clock(fakeClock).build();那本项目的代码为什么不用工厂方法?
本项目"双构造器 + 可见性差异"是一个合理的例外——因为它要利用包级可见性做测试专用入口(public锁真实时钟、包级开放假时钟),工厂方法反而表达不了"按调用方所在包限制访问"的语义。
八、速查表
| 规则 | 内容 |
|---|---|
| 出现位置 | 只能在构造器里 |
| 语句位置 | 必须第一条(否则编译错) |
| 调用目标 | 本类的另一个构造器 |
| 与 super(…) | 互斥,二选一 |
| 循环 | 直接/间接环都被编译器拒绝 |
| 省略时 | 编译器自动插入无参super() |
| 执行顺序 | 深到底(含父类),再逐层回 |
| 字段初始化 | 跟着自己那层走,委托会跳过委托层的初始化 |
| 语义 | 与this.xxx(实例引用)完全无关 |
| 适用规模 | ≤3 层;更多用工厂方法或 Builder |
九、总结
this(...)的全部知识点可以浓缩为三句话:
语法层:调用同一个类的另一个构造器,必须放在构造器的第一句。用来模拟 Java 缺失的"默认参数"功能。
工程层:字段赋值只在最底层的构造器出现一次,消除重复代码,新增字段不需要改多个构造器。
设计层:在本项目的代码中,通过
public和包级两个入口的可见性差异,把"生产用真实时钟、测试用假时钟"这条约束固化进类型系统,而不是靠口头约定。this.xxx里的this是"当前实例的引用";this(...)是另一套语法——构造器委托,跟实例引用无关,只是借用了同一个关键字。