PAYATHON 2026

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");

两种方案都会修改原对象的字段,并返回同一个对象。它们调整的是方法的返回类型声明,不会创建新对象。

备注:内容仅供参考。