今天在看代码的时候发现了一段奇怪的构造器代码——构造器里面居然有一行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.queryGatewayqueryGateway;this.commandGatewaycommandGateway;this.credentialGatewaycredentialGateway;this.XXXXXcationQueryGatewayXXXXXcationQueryGateway;this.currentTimeMilliscurrentTimeMillis;}}问题来了四参构造器里面的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.queryGatewayqueryGateway;this.commandGatewaycommandGateway;this.credentialGatewaycredentialGateway;this.XXXXXXcationQueryGatewayXXXXXXcationQueryGateway;this.currentTimeMillisSystem::currentTimeMillis;// 与下面重复}LotXXXXXXXXionDomainService(网关×4,LongSuppliercurrentTimeMillis){this.queryGatewayqueryGateway;// 重复this.commandGatewaycommandGateway;// 重复// ... 全部重复一遍this.currentTimeMilliscurrentTimeMillis;}问题很明显5 行赋值重复了两遍。以后加一个字段要改两个构造器漏改一个就会出现某条构造路径字段为 null的隐蔽 bug默认值System::currentTimeMillis散落在各处不好统一管理委托写法的好处赋值只有一个地方五参构造器是唯一干活的构造器四参构造器只是入口它的存在意义就是时钟参数默认为系统时钟新增字段只改五参构造器两个入口自动同步这其实就是 Java 版的**默认参数惯用法**。很多语言原生就支持默认参数比如 Pythondef__init__(self,queryGateway,...,currentTimeMillisSystem.currentTimeMillis):# ...Java 没有默认参数语法就用多个构造器 委托链来模拟参数少的构造器提供默认值然后转发给参数全的构造器。五、完整知识点清单知识点①必须是构造器的第一条语句publicA(){intx1;// 编译不通过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{privateintxinit();// 字段初始化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);}工厂方法的优势有名字、可以返回缓存/子类、不用暴露构造器。替代方案 BBuilder 模式参数多、可选组合多的时候使用A.builder().x(1).y(2).clock(fakeClock).build();那本项目的代码为什么不用工厂方法本项目双构造器 可见性差异是一个合理的例外——因为它要利用包级可见性做测试专用入口public锁真实时钟、包级开放假时钟工厂方法反而表达不了按调用方所在包限制访问的语义。八、速查表规则内容出现位置只能在构造器里语句位置必须第一条否则编译错调用目标本类的另一个构造器与 super(…)互斥二选一循环直接/间接环都被编译器拒绝省略时编译器自动插入无参super()执行顺序深到底含父类再逐层回字段初始化跟着自己那层走委托会跳过委托层的初始化语义与this.xxx实例引用完全无关适用规模≤3 层更多用工厂方法或 Builder九、总结this(...)的全部知识点可以浓缩为三句话语法层调用同一个类的另一个构造器必须放在构造器的第一句。用来模拟 Java 缺失的默认参数功能。工程层字段赋值只在最底层的构造器出现一次消除重复代码新增字段不需要改多个构造器。设计层在本项目的代码中通过public和包级两个入口的可见性差异把生产用真实时钟、测试用假时钟这条约束固化进类型系统而不是靠口头约定。this.xxx里的this是当前实例的引用this(...)是另一套语法——构造器委托跟实例引用无关只是借用了同一个关键字。
