ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java 设计模式:Curiously Recurring Template Pattern(CRTP)——用递归类型边界实现编译期静态多态

2026/10/1 2:34:55 拓冰建站 浏览量
Java 设计模式:Curiously Recurring Template Pattern(CRTP)——用递归类型边界实现编译期静态多态 示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载本指南以 java-design-patterns 仓库中 CRTP 模式的西班牙语技术文档 为核心骨架结合curiously-recurring-template-pattern模块的完整源码与测试系统讲解 Curiously Recurring Template Pattern奇异递归模板模式简称 CRTP在 Java 中的实现原理。阅读完本文你将掌握递归类型边界Recursive Type Bound的写法、CRTP 与普通继承多态的差异、以及如何用它在编译期约束方法参数必须是自身子类型从而写出类型安全的同类型交互代码如java.lang.Enum的compareTo。模式概览名称、别名与分类名称 / 分类Curiously Recurring Template Pattern属于**结构性Structural**模式在仓库中标注的标签为Extensibility可扩展性与Instantiation实例化。别名Recursive Type Bound递归类型边界、Recursive Generic递归泛型。英文版 模块 README 还补充了CRTP、Mixin Inheritance、Static Polymorphism静态多态等别名。意图Propósito允许派生组件从基组件继承某些与派生类型兼容的功能。换言之让基类中的方法签名能够引用正在继承它的那个具体子类类型。CRTP 并非一个复杂到难以理解的模式它的核心其实只有一行声明class Sub extends BaseSub——子类把自己作为泛型参数传给基类。正是这奇异地自我引用让编译器能够在编译期就精确知道方法的参数/返回类型应当落到哪个具体子类上。深入理解 CRTP从现实场景说起现实世界类比MMA 体重级别配对文档用综合格斗MMA赛事组织作为例子举办方要保证每场比赛都在同体重级别的选手之间进行绝不能让重量级拳手对阵雏量级拳手。这种同级别才能配对的强约束恰好就是 CRTP 在类型层面的表达——只有类型相同的对象才能互相调用某些方法。英文版 README 中还给出了另一个贴近日常的类比图书馆系统管理书籍、DVD、杂志等不同类型的媒体。它们共享借阅borrow、归还return等公共能力但每种媒体又有自身特有属性。用 CRTP 可以让基类MediaItemT承载公共方法而Book、DVD等子类以自身为类型参数继承它从而在避免虚拟方法开销的前提下定制共享行为。一句话概括让某个类型中的特定方法只接受其自身子类型的参数使多态行为在编译期就被确定下来。维基百科定义奇异递归模板模式CRTP最初源于 C是一种习惯用法idiom类 X 以自身 X 作为模板实参派生自某个类模板的实例化。Java 用泛型Generics实现了同样的思想MmaFighterT extends MmaFighterT中的T extends MmaFighterT被称为递归类型边界——类型参数 T 的边界bound本身又引用了携带该参数的泛型类。这类声明初看像循环定义但编译器能正确解析因为泛型边界允许这种自引用。静态多态 vs 动态多态值得特别说明的是Java 中常规的多态是动态多态——通过继承和方法重写override在运行时由虚方法表vtable决定调用哪个实现而 CRTP 提供的是静态多态Static Polymorphism——方法的最终签名在编译期就被钉死在具体子类型上无需运行时查找。这意味着类型约束在编译期即可校验错误越早暴露避免了对参数做instanceof或强制类型转换的运行时开销方法的参数类型对每个子类是专属的天然免疫把不同类型混进来的错误。程序化示例MMA 格斗家的源码实现以下代码均来自仓库模块 curiously-recurring-template-pattern 的src/main/java/crtp目录与文档示例完全对应。1. 泛型接口 Fighter首先定义泛型接口FighterT其中的fight方法接受一个类型为T的对手见 Fighter.javapublic interface FighterT { void fight(T t); }注意这里的T是将来由谁调用、谁就填谁的占位符。它没有约束任何类型都能实例化Fighter真正的约束发生在下一层的基类中。2. 核心基类MmaFighterT extends MmaFighterTMmaFighter用于创建按体重级别区分的格斗家它是整个模式的心脏见 MmaFighter.javaSlf4j Data public class MmaFighterT extends MmaFighterT implements FighterT { private final String name; private final String surname; private final String nickName; private final String speciality; Override public void fight(T opponent) { LOGGER.info({} is going to fight against {}, this, opponent); } }逐点剖析递归类型边界T extends MmaFighterT类型参数T必须是一个MmaFighter的直接或间接子类且该子类以自身为类型实参。这把谁能当格斗家限定为以自身实例化 MmaFighter 的那些类。implements FighterT接口中抽象的fight(T t)在此被实现为只接受同类型对手。对具体子类而言T会被替换成它自己。Lombok 注解源码使用了Slf4j生成LOGGER日志对象与Data自动生成 getter/setter、toString、equals、hashCode这就是为什么代码里可以直接写this打印出格斗家信息而不需要手写toString()。3. 体重级别子类把自己作为类型实参以下是MmaFighter的几个子类它们的关键写法完全一致在extends MmaFighter...的尖括号里填上自己的类名。雏量级见 MmaBantamweightFighter.javaclass MmaBantamweightFighter extends MmaFighterMmaBantamweightFighter { public MmaBantamweightFighter(String name, String surname, String nickName, String speciality) { super(name, surname, nickName, speciality); } }重量级见 MmaHeavyweightFighter.javapublic class MmaHeavyweightFighter extends MmaFighterMmaHeavyweightFighter { public MmaHeavyweightFighter(String name, String surname, String nickName, String speciality) { super(name, surname, nickName, speciality); } }轻量级见 MmaLightweightFighter.java结构完全相同。子类本身几乎不写逻辑只需要把构造参数透传给super(...)四个字段name、surname、nickName、speciality全部由基类持有——这正是从基类继承与派生类型兼容的功能的直观体现公共数据与公共行为下沉到基类类型差异由泛型参数表达。4. 入口程序同级别可战跨级别编译报错App.java 是模式运行入口完整演示了三种情况MmaBantamweightFighter fighter1 new MmaBantamweightFighter(Joe, Johnson, The Geek, Muay Thai); MmaBantamweightFighter fighter2 new MmaBantamweightFighter(Ed, Edwards, The Problem Solver, Judo); fighter1.fight(fighter2); // 雏量级 vs 雏量级编译通过运行正常 MmaHeavyweightFighter fighter3 new MmaHeavyweightFighter(Dave, Davidson, The Bug Smasher, Kickboxing); MmaHeavyweightFighter fighter4 new MmaHeavyweightFighter(Jack, Jackson, The Pragmatic, Brazilian Jiu-Jitsu); fighter3.fight(fighter4); // 重量级 vs 重量级同样没问题 // fighter1.fight(fighter3); // 雏量级挑战重量级直接编译错误最后一行被注释掉是有原因的由于MmaBantamweightFighter继承的fight(T)中T已被替换为MmaBantamweightFighter其签名等价于fight(MmaBantamweightFighter)传入MmaHeavyweightFighter时编译器直接报类型不匹配。不同体重级别无法开打这条业务规则被 CRTP 翻译成了编译期类型规则根本走不到运行时。编译期类型约束的底层机制源码级剖析递归类型边界为何合法MmaFighterT extends MmaFighterT乍看像鸡生蛋的循环但 Java 编译器按如下方式解析声明MmaFighter时T是一个尚未绑定的类型变量它的上界是MmaFighterT一种泛型引用自身的形式即所谓递归类型。只有当子类MmaBantamweightFighter extends MmaFighterMmaBantamweightFighter出现时T才被替换为具体类形成一个有限、可解析的闭合声明。Java 泛型规范明确允许这种自引用上界这也是Enum等标准库大量使用它的原因。类型擦除与桥接方法从 Java 泛型的擦除erasure机制来看MmaFighterT extends MmaFighterT擦除后fight(T opponent)的参数类型退化为边界类型MmaFighter。为了让MmaBantamweightFighter的fight(MmaBantamweightFighter)仍能正确实现Fighter接口编译器会为子类自动生成桥接方法bridge method一个签名为fight(MmaFighter)的方法内部把实参强制转换为MmaBantamweightFighter后再委托给真正的fight(MmaBantamweightFighter)。这一点可以从测试 FightTest.java 中运行时抛出ClassCastException的现象反推得到印证详见下文测试节。也就是说编译期的强类型约束在擦除后由编译器生成的桥接方法兜底保证运行时行为与类型声明一致。为什么跨级别调用在编译期就失败关键在于fight的静态类型。fighter1声明为MmaBantamweightFighter编译器据此把fighter1.fight(...)解析为参数类型MmaBantamweightFighter的重载版本传入fighter3类型MmaHeavyweightFighter既不是MmaBantamweightFighter也不是其子类于是产生编译错误。这种约束在写代码的瞬间IDE 里标红即可发现而不是等到运行时报错——这是 CRTP 相比运行时用instanceof判断类型方案的核心优势。运行与测试验证单元测试 FightTest同级别配对约束仓库的 FightTest.java 用 JUnit 5 验证了模式的真实行为测试构造一个雏量级选手fighter再准备一个包含雏量级、轻量级、重量级选手的混合对手列表ListMmaFighter?逐个尝试((MmaBantamweightFighter) challenger).fight(fighter)当challenger实际是雏量级时调用成功选手被记入challenged列表当challenger是轻量级或重量级时强制转换失败抛出ClassCastException被catch捕获并记入日志最后断言challenged列表非空且其中所有元素都是MmaBantamweightFighter实例。assertFalse(challenged.isEmpty()); assertTrue(challenged.stream().allMatch(c - c instanceof MmaBantamweightFighter));这段测试极好地说明了 CRTP 的一个边界一旦通过通配符MmaFighter?把具体类型擦掉编译器就无法再校验具体体重级别运行时只能靠instanceof和强制转换兜底失败即抛ClassCastException。它提醒我们——CRTP 的类型安全建立在始终以具体子类类型引用对象的前提上泛型通配符会在一定程度上削弱这种保护。冒烟测试 AppTestAppTest.java 是一个简单的冒烟测试assertDoesNotThrow(() - App.main(new String[]{}))验证入口程序在默认参数下可无异常运行即两个合法级别的对战能正常完成。如何运行模块为独立的 Maven 工程含 pom.xml仓库根目录提供 Maven Wrappermvnw。可以按以下方式在本地运行与测试# 在仓库根目录执行编译并运行单元测试 ./mvnw test -pl curiously-recurring-template-pattern # 直接运行模式演示入口 ./mvnw exec:java -pl curiously-recurring-template-pattern \ -Dexec.mainClasscrtp.App运行App.main后控制台会输出两条日志来自基类MmaFighter.fight中的LOGGER.info例如Joe The Geek Johnson is going to fight against Ed The Problem Solver Edwards Dave The Bug Smasher Davidson is going to fight against Jack The Pragmatic Jackson类图与流程图文档中给出了 CRTP 的类图crtp.png展示了FighterT接口、MmaFighterT extends MmaFighterT基类及其各体重级别子类之间的继承与实现关系英文版 模块 README 则提供了模式整体决策流程的示意图crtp-flowchart.png两图可与源码相互印证。CRTP 类图Fighter 接口、MmaFighter 递归泛型基类与体重级别子类的关系CRTP 模式流程图适用场景Applicability根据文档当出现以下情况时应考虑使用 CRTP在对象继承层级中链式调用方法时存在类型冲突。典型如 Fluent Builder / Fluent API若基类方法返回Base子类链式调用时会丢失子类类型用Self即 CRTP让每个子类返回自身类型链式调用不再需要强制转换。希望使用参数化的类方法让它接受该类的子类作为参数从而作用于继承自该类的所有对象——例如 MMA 示例中的fight(T opponent)T精确限定为调用者的同类。希望某些方法只作用于同类型实例典型需求是实现互相比较comparability——让compareTo、equals等方法只接受同类型的比较对象避免与不同类型对象比较产生运行时ClassCastException。需要留意的是CRTP 适合类型之间需要精确、同质交互的场景如果业务上本来就需要异构对象协作或需要把对象放入ListMmaFighter?这类通配符容器再按需转换则 CRTP 的编译期保护会部分失效需要配合instanceof等运行时手段正如测试所展示的那样。已知使用java.lang.Enum 与 Comparable 一族文档明确指出CRTP 在 Java 标准库中最著名的应用是java.lang.Enum其声明为public abstract class EnumE extends EnumE implements ComparableE, Serializable它正是类 X 以自身为类型实参派生自泛型类的标准形式。这带来的实际收益包括类型安全的compareToEnum的compareTo(E other)只接受同枚举类型的参数。Color.RED.compareTo(Size.LARGE)会在编译期直接报错而不是运行期抛异常valueOf等静态方法的返回类型推导Enum.valueOf(ClassT enumType, String name)利用边界保证返回的正是目标枚举类型getDeclaringClass返回类型精确化子类能以自身类型精确声明方法返回类型。此外ComparableT配合T为实现类自身的用法如class Person implements ComparablePerson是同一思想的日常化变体被《Effective Java》第 5 章泛型部分的 Item 30优先使用泛型方法作为推荐实践讨论。在 Java 生态中凡是需要只与同类比较/交互的领域模型实体比较、版本比较、区间运算等都可以套用这一模式。参考资料与延伸阅读本文核心文档localization/es/crtp/README.md西班牙语版 CRTP 说明英文版完整说明curiously-recurring-template-pattern/README.md全部源码curiously-recurring-template-pattern/src/main/java/crtp测试用例curiously-recurring-template-pattern/src/test/java/crtp文档Créditos致谢部分还推荐了两份经典资料此处仅作文字指引未附外链NuaH 的个人博客中关于 CRTP 的文章、Stack Overflow 上Recursive type bound 在泛型中是什么意思的高票回答以及 Angelika Langer 的 Java Generics FAQFAQ 106如何理解EnumE extends EnumE。赞分享示例工程教程【免费下载链接】java-design-patternsDesign patterns implemented in Java项目地址https://gitcode.com/GitHub_Trending/ja/java-design-patterns点击查看免费下载相关推荐Java 中的奇异递归模板模式CRTP用 java-design-patterns 项目实战掌握编译期静态多态Java 中的奇异递归模板模式CRTP用 java design patterns 项目实战掌握编译期静态多态 奇异递归模板模式Curiously Re示例工程教程Fate/Grand Order终极助手Chaldea素材规划与战斗模拟完整指南Fate/Grand Order终极助手Chaldea素材规划与战斗模拟完整指南 Chaldea是一款专为Fate/Grand Order玩家设计的跨平台辅助桌面应用移动开发C静态多态编译期多态的实现方法C静态多态编译期多态的实现方法 你是否还在为运行时多态的性能开销而困扰编译期多态Compile time Polymorphism通过模板技术在编译文档教程上一篇终极英雄联盟回放分析工具ROFL-Player完整使用指南下一篇Visual C运行库终极指南5分钟彻底解决Windows软件兼容性问题创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考