Java 中如何让继承的方法返回当前子类类型,以支持链式调用?
可以在子类中重写 domain,用协变返回类型让它返回子类类型;也可以给父类添加泛型参数,用来表示子类类型。
题目中的代码直接重写方法即可。如果父类的链式方法和子类都比较多,用泛型可以减少重复重写。
为什么 return this 仍然不能继续调用子类方法?
原来的 domain 返回的就是当前对象,调用后仍然是同一个 NSRecord 实例。但方法声明的返回类型是 DNSRecord:
public DNSRecord domain(String domain)
编译器根据声明的返回类型检查后续调用,所以 record.domain("pasta.org") 的编译期类型是 DNSRecord。这个类型没有 nameserver 方法,链式调用因此无法通过编译。即使方法返回的是 this,Java 也不会自动把返回类型调整为调用对象的子类类型。
方案一:在子类中重写方法
Java 支持协变返回类型,重写方法时可以返回父类方法返回类型的子类型。
保留原来的 DNSRecord,修改 NSRecord:
public final class NSRecord extends DNSRecord {
private String nameserver;
public NSRecord(String nameserver) {
this.nameserver = nameserver;
}
@Override
public NSRecord domain(String domain) {
super.domain(domain);
return this;
}
public NSRecord nameserver(String nameserver) {
this.nameserver = nameserver;
return this;
}
}
这样就可以继续调用 nameserver:
NSRecord record = new NSRecord("ns0.somedomain.net")
.domain("pasta.org")
.nameserver("ns1.somedomain.net");
因为 NSRecord.domain 声明返回 NSRecord,编译器允许继续调用它的 nameserver 方法。
CNAMERecord 也可以重写 domain,将返回类型改为 CNAMERecord。这种写法容易理解,不过,凡是需要返回子类类型的父类方法,都要在各个子类中重写。
方案二:用递归泛型声明返回类型
要让 domain 只在父类中实现,可以让子类通过泛型参数将自身类型传给父类。
父类改为:
public abstract class DNSRecord<T extends DNSRecord<T>> {
protected String domain;
protected abstract T self();
public T domain(String domain) {
this.domain = domain;
return self();
}
}
NSRecord 继承 DNSRecord<NSRecord>:
public final class NSRecord extends DNSRecord<NSRecord> {
private String nameserver;
public NSRecord(String nameserver) {
this.nameserver = nameserver;
}
@Override
protected NSRecord self() {
return this;
}
public NSRecord nameserver(String nameserver) {
this.nameserver = nameserver;
return this;
}
}
调用方式相同:
NSRecord record = new NSRecord("ns0.somedomain.net")
.domain("pasta.org")
.nameserver("ns1.somedomain.net");
这里将 T 指定为 NSRecord,继承的 domain 方法在编译期的返回类型也就是 NSRecord。
CNAMERecord 则传入自己的类型:
public final class CNAMERecord extends DNSRecord<CNAMERecord> {
@Override
protected CNAMERecord self() {
return this;
}
}
因此,下面的赋值也成立:
CNAMERecord record = new CNAMERecord()
.domain("www.pasta.org");
以后在父类中添加链式方法,也可以将返回类型声明为 T,通过 self() 返回当前对象,无须在每个子类中重复重写。
使用时需要注意什么?
泛型参数需要遵守“传入自身类型”的约定
T extends DNSRecord<T> 是递归类型约束,但不能强制要求 T 就是当前子类。因此,子类需要像示例中那样传入自身类型,并在 self() 中返回 this。
将具体子类声明为 final,可以避免继续派生后,继承的链式方法仍返回上一层类型的问题。如果需要多层继承,中间类也要继续传递泛型参数,或在下一层重写方法。
使用抽象 self() 可以避免未检查的强制转换
有些写法会直接在父类中返回:
return (T) this;
这种转换会产生 unchecked cast 警告,因为编译器无法证明 this 一定属于 T。使用抽象 self(),由具体子类直接返回 this,就不需要这次强制转换。
调用表达式的编译期类型仍然重要
采用第一种方案时,如果变量声明为父类类型:
DNSRecord record = new NSRecord("ns1.somedomain.net");
record.domain("pasta.org");
即使运行时执行的是 NSRecord 的重写方法,record.domain(...) 的编译期类型仍是 DNSRecord,无法直接接着调用 nameserver。
采用泛型方案时,可以保留具体类型信息:
DNSRecord<NSRecord> record =
new NSRecord("ns0.somedomain.net");
record.domain("pasta.org")
.nameserver("ns1.somedomain.net");
两种方案都会修改原对象的字段,并返回同一个对象。它们调整的是方法的返回类型声明,不会创建新对象。
备注:内容仅供参考。
