CRTP简述

CRTP(Curiously Recurring Template Pattern)是一种通过模板继承实现的静态多态技术,其核心思想是一个类(通常为基类)将其派生类作为模板参数,从而在编译时实现多态行为。其基本形式为:

template <typename Derived>
class Base {
    // 在 Base 内部可以使用 Derived 类型的接口
};

class Derived : public Base<Derived> {
    // ...
};

通过模板参数将派生类的类型信息注入到基类中,使基类能够根据派生类型提供不同的行为,而这一切都在编译时完成,避免了运行时的虚函数开销。它实现了所谓的静态多态(编译时多态),与虚函数的动态多态形成对比。

设计目的:
1. 代码复用,将公共的接口或算法提取到基类中,派生类只需提供少量定制点。
2. 静态多态,在编译时决定调用哪个函数,避免虚函数表查找和间接调用的开销。
3. 编译时“自引用”能力,基类能够访问派生类的成员(通过 static_cast 转换 this 指针)。
4. 扩展已有类型的功能,在不修改类型定义的情况下添加通用功能。

在基类中,可以通过 static_cast(this) 将 this 指针转换为派生类指针,从而调用派生类的成员函数。这要求基类的成员函数是模板函数或内联函数,并且在实际使用时 Derived 类型是完整的。使用示例如下:

template <typename Derived>
class Base {
public:
    void interface() {
        // 将 this 转换为派生类指针,调用实现
        static_cast<Derived*>(this)->implementation();
    }

    // 也可以提供默认实现
    void commonFunc() {
        // 一些公共代码
    }
};

class Derived1 : public Base<Derived1> {
public:
    void implementation() { std::cout << "Derived1 impl\n"; }
};

class Derived2 : public Base<Derived2> {
public:
    void implementation() { std::cout << "Derived2 impl\n"; }
};

Derived1 d1;
d1.interface(); // 输出 Derived1 impl
Derived2 d2;
d2.interface(); // 输出 Derived2 impl

可以看出,相较于两个派生类均重写一次模板类的 interface() 方法来调用自身的 implementation() 方法, CRTP 设计思想能够在模板类中直接调用派生类的方法从而简化样板代码,提高代码复用和扩展的能力。

在Java实体类的应用

Java的实体类一般由属性、构造器、@Data(Lombok)以及其他的注解、业务方法组成:

/**
 * 
 * @description 用户实体类
 * @author FFlamingO
 * @date 2026-03-04 9:42:56
 * @version 1.0.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long userID;
    private String userName;
    private String userEmail;
    private String userPassword;
    private String userAvatar;
    private Timestamp userCreateTime;
    private Timestamp userLastLogin;
    private Boolean userGrade;
}

为了简化实体类的初始化,会采用“定义部分属性为参数的构造器”或“添加链式调用方法”。如 Lombok 提供的 @Accessors(chain = true) ,此时可以通过如下方式进行实体类对象的数据初始化:

// 原始方案
User fflamingo = new User();
fflamingo.setUserName("FFlamingO");
fflamingo.setUserGrade(true);
// 链式调用
new User().setUserName("FFlamingO").setUserGrade(true);

当业务要求在数据库中存储除实体类属性外,还有通用字段如"seq", "createdTime", "updateTime"等信息时,会将通用字段定义在实体类基类中,由实体类继承基类。在这种场景下,由于Lombok的注解限制为在当前类中生效,因此若需链式调用,必须在基类和子类均加上 @Accessors(chain = true) 注解,子类继承基类由 @Accessors 生成的方法后能够链式调用设置继承于基类的属性:

public class DefaultEntity {
    protected Long seq;
    protected LocalDateTime createdAt;
    protected LocalDateTime updatedAt;
    protected Integer version;
    protected LocalDateTime deletedAt;
}
public class User extends DefaultEntity {}
new User().setSeq(1L).setUserName("FFlamingO");

想法很美好,但同样因 @Accessors 注解自身实现,基类的如 setSeq() 方法的返回值为基类自身 DefaultEntity.class,而基类并不能够识别子类方法的存在 Cannot resolve method 'xxx' in 'DefaultEntity'。

在该场景下,CRTP中”将派生类作为模板参数“的思想能够很好地解决问题。Java中能够实现该思想的方案之一即为 泛型 <T extends ?>。为基类设置泛型 DefaultEntity<T extends DefaultEntity<T>> 指定泛型的类型必须为基类的子类,允许基类将子类作为类型参数,从而在基类的方法中引用具体的子类类型。
若想要成功实现上述的链式调用,还需要修改基类中的Setter方法(Lombok无法自动生成):

public class DefaultEntity<T extends DefaultEntity<T>>
public class User extends DefaultEntity<User>

public T setSeq(Long seq) {
    this.seq = seq;
    return (T) this;
}

// @Accessor
public DefaultEntity setSeq(Long seq) {
    this.seq = seq;
    return this;
}

与 Lombok 自动生成的方法不同,CRTP方案因给基类传递了子类类型,因此可以通过类型转换将基类的this转换为子类类型。

同理,在设计其他的通用方法时,也可以通过泛型实现类型安全的返回类型,但由于Java语言泛型通过类型擦除实现,且方法调用默认为动态绑定,无法直接通过类型转换来调用子类的方法,但可以通过抽象方法、接口默认方法(运行时多态)或函数式接口实现类似的效果。

我想活得聪明,通透,快乐,满足。 我想我的世界窗明几净。 一片雾蓝色。
最后更新于 2026-03-04