Java模板模式实战:从原理到框架源码与面试考点
你是不是也有这种感觉很多代码写着写着发现公共流程都差不多只是中间某一步不一样于是把公共代码抽到父类子类各自实现自己的逻辑。这个套路你其实一直在用只是没意识到这就是Java里的模板模式Template Pattern。模板模式可以说是设计模式里被低估的一个。它不像单例、工厂那样被频繁挂在嘴边但几乎所有主流框架源码里都有它的身影。Spring的JdbcTemplate、RestTemplateJDK里的AbstractList、InputStream底层骨架全是模板模式的思想。你只要搞懂它读框架源码会轻松很多面试遇到“设计模式”相关的问题也不至于只讲个单例工厂凑数。这篇文章我打算把模板模式讲透从它解决了什么问题开始到怎么写一个完整示例再到源码里怎么识别它最后把实际开发中最容易踩的坑和面试常考的点都梳理一遍。无论你是刚学Java基础还是准备java面试八股文的老手都能从里面挖到点东西。1. 重新认识模板模式它不只是把公共代码抽个父类1.1 三个角色搞清楚模板模式的类结构模板模式的定义很简洁在一个方法中定义算法的骨架将某些步骤延迟到子类中实现。它可以让子类在不改变算法结构的情况下重新定义算法中的某些特定步骤。这里有个很容易被忽略的关键词骨架。不是说把公共代码塞进父类就完事了而是父类要负责编排“流程”子类只负责“填空”。具体来说模板模式一般有三个角色角色名称职责AbstractClass抽象父类定义模板方法和抽象步骤方法ConcreteClass具体子类实现父类中声明的抽象步骤Template Method模板方法定义流程骨架通常用final修饰举一个最经典的例子做咖啡和做茶。整个过程其实很相似都是烧水、冲泡、倒入杯子、加调料。但“冲泡什么”“加什么调料”不一样。这时候把公共流程抽出来把变化的部分做成抽象方法就是模板模式最直观的样子。public abstract class Beverage { // 模板方法定义制作饮料的完整流程 public final void prepare() { boilWater(); brew(); pourInCup(); addCondiments(); } // 烧水和倒杯子是所有饮料都一样的直接实现 private void boilWater() { System.out.println(烧水); } private void pourInCup() { System.out.println(倒入杯子); } // 冲泡和加调料则交给子类 protected abstract void brew(); protected abstract void addCondiments(); } public class Coffee extends Beverage { Override protected void brew() { System.out.println(冲泡咖啡粉); } Override protected void addCondiments() { System.out.println(加糖加奶); } } public class Tea extends Beverage { Override protected void brew() { System.out.println(泡茶叶); } Override protected void addCondiments() { System.out.println(加柠檬); } }子类只需要关心两件事泡什么、加什么。至于烧水、倒杯子这种固定动作父类已经写死了子类碰都不用碰。1.2 模板方法为什么必须加final防止骨架被破坏很多初学者写模板模式的时候模板方法忘记加final或者压根不知道为什么要加。这是个很关键的设计决策。你想想模板方法的作用是固定算法骨架保证流程不被破坏。如果子类可以随便重写prepare()把冲泡步骤放到加调料后面那整个模板模式的“骨架复用”就失去意义了。所以我在代码里给prepare()加了final也是在传递一个信号这个方法是流程定义你别动它。注意final不是模板模式的强制语法要求但它是模板模式的“设计意图”保护机制。实际开发中我建议模板方法一律加final除非你有非常特殊的理由允许子类改写整个流程。这有点像自助餐厅的规定进门拿餐盘、选菜、结账、找座位这个流程是餐厅定好的你作为顾客只需要决定“选什么菜”不能把结账挪到选菜前面。1.3 钩子方法让模板骨架长出弹性有一种情况很常见大部分子类都需要某个步骤但有个别子类不需要。这时候如果把它做成抽象方法那不需要它的子类也得写个空实现很别扭。解法是引入钩子方法Hook Method。钩子方法的本质是父类提供一个默认实现往往是空方法或返回true/false子类按需覆盖从而影响模板方法中的某些步骤是否执行。public abstract class Beverage { public final void prepare() { boilWater(); brew(); pourInCup(); // 钩子方法决定是否加调料 if (customerWantsCondiments()) { addCondiments(); } } protected boolean customerWantsCondiments() { return true; // 默认加调料 } protected abstract void brew(); protected abstract void addCondiments(); } public class PureTea extends Beverage { Override protected void brew() { System.out.println(泡茶叶); } Override protected void addCondiments() { System.out.println(加柠檬); } // 子类重写钩子方法我不加调料 Override protected boolean customerWantsCondiments() { return false; } }这样“纯茶”就不用被迫写一个空的加调料方法了。钩子方法给了模板模式一种“可裁剪”的能力很多框架里都有这种用法比如Spring的模板类经常用钩子方法让使用方决定某些初始化的细节。2. 手写一个完整示例数据导出流程的模板化改造2.1 场景设定订单导出和用户导出流程相同数据不同我之前维护过一个后台管理系统里面有各种导出功能订单导出、用户导出、商品导出。刚接手的时候每个导出都是独立的Service代码结构大同小异但关键流程都是一样的。导出流程无非五步校验导出参数根据参数查询数据把数据转换成目标格式比如CSV生成文件记录导出日志这五步里第二步和第三步因业务而异其他步骤基本可以复用。这就是一个特别标准的模板模式应用场景。我当时重构的时候没有直接把公共代码复制到一个工具类里而是设计了抽象父类把流程固定下来。2.2 从抽象类到子类把过程写给你看先定义抽象导出器public abstract class AbstractDataExporter { // 模板方法完整导出流程 public final void export(String params) { validate(params); ListMapString, Object data queryData(params); String content convert(data); writeToFile(content); } // 公共步骤参数校验 protected void validate(String params) { if (params null || params.isEmpty()) { throw new IllegalArgumentException(导出参数不能为空); } System.out.println(参数校验通过); } // 抽象步骤查询数据业务自己实现 protected abstract ListMapString, Object queryData(String params); // 抽象步骤转换为导出格式业务自己实现 protected abstract String convert(ListMapString, Object data); // 公共步骤写文件 private void writeToFile(String content) { System.out.println(生成导出文件); System.out.println(content); } }然后订单导出器和用户导出器各自实现自己关心的部分public class OrderExporter extends AbstractDataExporter { Override protected ListMapString, Object queryData(String params) { System.out.println(根据参数查询订单数据 params); return List.of( Map.of(orderId, 1001, amount, 99.50), Map.of(orderId, 1002, amount, 199.00) ); } Override protected String convert(ListMapString, Object data) { StringBuilder sb new StringBuilder(订单编号,金额\n); for (MapString, Object row : data) { sb.append(row.get(orderId)).append(,) .append(row.get(amount)).append(\n); } return sb.toString(); } } public class UserExporter extends AbstractDataExporter { Override protected ListMapString, Object queryData(String params) { System.out.println(根据参数查询用户数据 params); return List.of( Map.of(userId, U001, name, 张三), Map.of(userId, U002, name, 李四) ); } Override protected String convert(ListMapString, Object data) { StringBuilder sb new StringBuilder(用户ID,姓名\n); for (MapString, Object row : data) { sb.append(row.get(userId)).append(,) .append(row.get(name)).append(\n); } return sb.toString(); } }使用的时候public class ExportClient { public static void main(String[] args) { AbstractDataExporter orderExporter new OrderExporter(); orderExporter.export(2025-01-01,2025-01-31); AbstractDataExporter userExporter new UserExporter(); userExporter.export(2025-01-01,2025-01-31); } }输出结果参数校验通过 根据参数查询订单数据2025-01-01,2025-01-31 生成导出文件 订单编号,金额 1001,99.50 1002,199.00 参数校验通过 根据参数查询用户数据2025-01-01,2025-01-31 生成导出文件 用户ID,姓名 U001,张三 U002,李四这个例子最大的价值在于新增一种导出类型的时候你只需要继承AbstractDataExporter实现“查数据”和“转格式”两个方法然后走人。不需要关心参数校验、文件生成这些公共逻辑更不会因为忘记校验参数导致导出空数据。如果你还想更灵活一点可以在模板方法里加一个钩子方法比如有的导出需要加文件头有的不需要默认钩子返回空字符串即可。这里就不再展开写了思路是一样的。2.3 不用模板模式会怎样复制粘贴的灾难有人可能会说这点逻辑用不上设计模式吧我直接写个工具类Util把公共流程封装成一个静态方法不就行了比如ExportUtil.export(params, queryFunction, convertFunction)。这个思路也不是不行尤其是在函数式接口普及之后确实可以替代一部分模板模式。但模板模式有一个工具类替代不了的特点子类可以在守住骨架的前提下天然地扩展自己的内部逻辑。比如子类可以重写validate方法加上订单特有的校验而工具类那种“传函数”的方式每个差异点都得靠函数参数传递代码会变得很啰嗦。模板模式真正解决的问题不是“公共代码复用”而是“业务扩展点和流程控制权的统一管理”。这在多人协作的大项目里尤其明显。只要骨干方法足够清晰新人接一个新业务照着父类注释写两个抽象方法就完事不容易写歪。3. JDK和主流框架里的模板模式源码观察3.1 在JDK源码里找模板模式AbstractList和InputStream很多人学设计模式觉得抽象是因为光看不练。其实JDK里的源码就是最好的教材。先说AbstractList。它是所有List实现类的抽象父类ArrayList、LinkedList都继承自它。AbstractList里定义了一个抽象方法get(int index)但它不是让你只实现get就结束它还基于get实现了一堆通用方法比如indexOf、contains、iterator、subList等。public abstract class AbstractListE extends AbstractCollectionE implements ListE { abstract public E get(int index); public int indexOf(Object o) { ListIteratorE it listIterator(); if (onull) { while (it.hasNext()) if (it.next()null) return it.previousIndex(); } else { while (it.hasNext()) if (o.equals(it.next())) return it.previousIndex(); } return -1; } }看到没有AbstractList把“按索引查找元素”的流程写死在indexOf里ArrayList和LinkedList只需要各自实现get即可查找逻辑完全复用。这就是模板模式在JDK里的典型应用。再看InputStream。它有一个抽象方法read()每次读一个字节。同时它又提供了read(byte[], int, int)内部就是循环调用read()把数据读满。public abstract class InputStream implements Closeable { public abstract int read() throws IOException; public int read(byte b[], int off, int len) throws IOException { // 省略部分边界检查 for (int i 0; i len; i) { int c read(); if (c -1) { return i; } b[off i] (byte)c; } return len; } }所有InputStream的子类比如FileInputStream、ByteArrayInputStream都只需要实现最核心的read()方法就能自动具备“批量读取”的能力。父类把流程写死子类填空这就是模板模式最经典的传承。3.2 Spring的JdbcTemplate模板方法回调的经典组合Spring里的JdbcTemplate可能是模板模式在框架应用中最具代表性的案例。它把JDBC操作的固定流程——获取连接、创建Statement、执行SQL、处理ResultSet、清理资源——全部封装在内部暴露给使用者的只是一个回调接口。public T T execute(StatementCallbackT action) throws DataAccessException { // 获取连接 Connection con DataSourceUtils.getConnection(obtainDataSource()); Statement stmt null; try { // 创建Statement stmt con.createStatement(); // 调用回调执行SQL并处理结果 T result action.doInStatement(stmt); return result; } catch (SQLException ex) { // 异常转换 throw translateException(JdbcTemplate.execute, sql, ex); } finally { // 清理资源 JdbcUtils.closeStatement(stmt); DataSourceUtils.releaseConnection(con, obtainDataSource()); } }使用者不需要关心Connection怎么获取、Statement怎么关闭、异常怎么转换只需要在回调里写自己的业务逻辑String name jdbcTemplate.execute(statement - { ResultSet rs statement.executeQuery(SELECT name FROM user WHERE id 1); rs.next(); return rs.getString(name); });这里JdbcTemplate本身是模板类execute方法是模板方法而StatementCallback是回调接口也就是子类的替代品。为什么Spring不直接用继承实现模板模式而要用回调组合答案很现实Java是单继承一个类如果继承了JdbcTemplate就不能再继承其他业务基类了。用回调接口使用者可以选择任何方式组织自己的代码灵活度更高。这也说明框架设计者在应用模板模式时会根据语言的特性做出变通。3.3 快速识别模板模式的三个特征如果你在读源码的时候想快速判断一个类是否用了模板模式看三个特征就行父类有一个非抽象方法方法内部调用了抽象方法。这是最核心的特征。如果父类里没有非抽象方法去调用抽象方法那只是普通的抽象类不是模板模式。抽象方法由子类实现嵌套在父类的固定流程里。子类不能控制调用顺序只能提供一个具体的实现。模板方法往往用final修饰或者至少从命名、注释上能看出“这个方法定义了算法骨架”。掌握了这三点你去看Spring、MyBatis、Netty等框架的源码会经常发现模板模式的身影。4. 模板模式的优缺点与选型判断别把模式用反了4.1 模板模式真正解决的是什么问题模板模式解决的说到底就是“复用不变隔离变化”的问题。它把算法中不变的部分沉淀到父类把变化的部分留给子类同时通过父类控制整体流程。这对代码维护有几点实实在在的好处流程不可破坏。因为模板方法是final所有子类必须遵循相同的算法骨架不会有人在不经意间把公共流程改乱。避免重复代码。公共步骤写在父类所有子类共享。扩展点清晰。每个抽象方法都是一个扩展点新来的人照着重写就行。符合开闭原则。新增一个业务只需要新增一个子类不改动已有代码。4.2 模板模式的优点与局限优点不再重复重点说说它的局限局限说明单继承限制Java类只能继承一个父类如果为了套模板模式强行继承抽象类会牺牲掉其他继承机会类数量膨胀每个业务变体至少要新增一个子类小场景下类会越来越多抽象方法过多时子类负担重如果父类定义了七八个抽象方法子类即使只用两个也得全部实现可以用适配器类缓解父类与子类的耦合较紧密子类依赖父类的流程定义如果父类流程改了所有子类都有可能受影响4.3 模板模式 vs 策略模式继承和组合的选择模板模式和策略模式经常被拿来比较两者解决的问题确实有一点点重叠但思路完全不同。模板模式是基于继承的复用父类定义流程骨架子类填充具体步骤。整个算法的流程结构在父类里是固定的子类只能替换其中的某几步。控制权在父类手里。策略模式是基于组合的复用一个完整的算法被封装成独立的策略对象客户端持有策略对象运行时可替换。控制权在使用方手里。举个直观的例子如果你有“订单导出、用户导出、商品导出”三种导出它们的流程都是“校验、查数、转换、写文件”只有细节不同这就是模板模式的菜。如果你的服务支持多种支付方式每种支付方式的整套算法包括风控、签名、支付、回调验签都不一样而且客户可能希望运行时切换支付方式这时候策略模式更合适。维度模板模式策略模式代码组织继承组合流程控制权父类定义骨架客户端选择策略复用粒度局部步骤的复用整个算法的替换扩展方式新增子类重写抽象步骤新增策略实现类切换引用典型场景流程固定、细节可变的业务算法整体变化、运行时切换一句话总结流程骨架固定只是部分步骤有差异用模板模式整个算法都可能换要运行时切换用策略模式。5. 资深开发者的模板模式实践指南与避坑清单5.1 设计模板方法时我坚持的四个习惯模板模式简单归简单但真正在项目里写出好用的模板类还是有点讲究的。我总结了自己比较看重的几个习惯。第一模板方法一律加final。这一点前面说过不再啰嗦。宁可让子类用钩子方法来影响流程也不要让子类直接改流程。第二抽象步骤的粒度要合适。这个粒度挺难掌握的。粒度太粗子类每一块都很大公共代码没法充分下沉粒度太细子类被迫实现一堆没意义的方法。我的经验是一个抽象方法尽量只做一件事但这件事的规模应该等同于“一个业务流程里的一个环节”。比如“校验参数”“查询数据”“转换格式”这些都是合适的粒度。第三给每个抽象方法写清楚注释。注释里至少说明三件事这个步骤在流程中的位置、正常情况下应该做什么、有没有可以不实现的钩子变体。这一点很多团队会忽略结果子类实现者只能靠猜。第四尽量把“顺序敏感”的代码封装在父类私有方法里不要暴露给子类。子类只管填内容不需要知道步骤之间的先后关系。5.2 实际开发中容易踩的坑一段一段讲清楚模板模式看着简单实际操作中坑不少。我随便列几个自己踩过或者在代码评审里见过的。坑一模板方法忘了加final子类把它重写了。这个很常见。某个子类开发发现流程和需求“有一点点不同”就直接重写模板方法从父类复制了一套代码改改后面维护的时候公共逻辑改了一处另外一处没跟上bug就来了。模板方法加final从语法层面杜绝这种情况。坑二构造函数里调用抽象方法。这是Java对象初始化顺序的经典问题。如果你在抽象父类的构造函数里调用了一个抽象方法比如init()那么在创建子类对象时父类构造函数会先执行此时子类的字段还没初始化你调用的init()里如果访问了子类字段拿到的是null或者默认值。public abstract class Parent { public Parent() { init(); // 危险写法 } protected abstract void init(); } public class Child extends Parent { private String name child; Override protected void init() { // 此时name还是null System.out.println(name.length()); } }运行会抛NullPointerException。这个坑非常隐蔽我建议所有抽象父类的构造器里都不要调用可被子类重写的方法。如果你确实需要在初始化阶段做点事情可以提供doInit()方法让子类重写并在注释里明确说明“务必在处理完整字段后再调用父类方法”。坑三钩子方法命名不清导致实现者误解。比如钩子方法叫isSupport()子类实现者可能以为返回true是“这个功能被支持”返回false是“不支持”但父类内部的判断逻辑可能是反的。所以钩子方法的命名一定要语义化而且要在注释里写清楚返回值的含义。我习惯在注释里直接写return true表示执行调料添加false表示跳过这种注释。坑四抽象方法过多子类负担太重。如果一个抽象类定义了太多抽象方法子类实现的时候代码里一大半都是不得不写又不用的空方法。这种情况要么是抽象粒度没设计好要么是该拆分成多个接口。我见过一些新手项目父类里塞了十几个抽象方法子类实现一个业务要写几百行空实现这已经不是模板模式了是模板模式的滥用。坑五过度设计只有一个实现类的时候也在用模板模式。模板模式的价值在于“复用公共流程 隔离多个变体”。如果当前只有一个子类未来大概率也不会加新的那不如先用普通类写清楚等出现第二个变体的时候再抽象也不迟。设计模式不是越早用越好是用在“确实需要抗变化”的地方。坑六模板方法里的步骤之间有隐藏依赖。比如“查询数据”这个抽象方法子类可能依赖“校验参数”阶段设置的某个状态而父类的模板方法如果改了步骤顺序就会破坏这种隐式依赖。模板方法里的每一步通过参数传递信息尽量避免子类之间通过父类字段共享状态。这样步骤顺序的调整才不会被隐式绑定。5.3 代码评审时怎么看模板模式代码评审时我一般会重点看三点一看模板方法是否是final有没有被子类重写的风险。如果看到模板方法没有final我会提醒开发者补上或者要求说明理由。 二看抽象方法的粒度是否合适子类实现是否出现大量空方法。 三看具体子类是否只做了“填空”有没有把业务逻辑写进公共方法覆盖掉。如果某个子类大面积重写父类的非抽象方法说明父类的骨架设计可能有问题或者这个子类的业务流程本质上就不一样不应该硬套模板模式。6. 面试八股文模板模式考点速答与手撕实现6.1 “模板模式和策略模式有什么区别”怎么答这是面试出现频率最高的问题。面试官问这个不是想听你背定义而是想看你能不能把设计模式用在实际场景里。回答的思路我建议分成三层第一层说本质模板模式用继承父类定义流程子类实现具体步骤策略模式用组合把整个算法封装成策略对象客户端可以切换。 第二层说控制权模板模式的控制权在父类流程是固定的策略模式的控制权在客户端算法整体可以替换。 第三层举例说明比如导出数据流程固定用模板模式支付方式可切换用策略模式。这样回答既有深度又接地气面试官听得出你是真用过。6.2 “模板方法为什么要用final”怎么答直接答核心final是为了防止子类重写模板方法从而保证算法骨架不被破坏。如果子类随便改流程模板模式“复用流程、隔离变化”的意义就没了。然后可以补一句这也是模板模式和普通继承的区别模板模式强调的是“骨架的稳定性”。如果你还想加分可以提一下钩子方法当需要让子类影响流程时应该在骨架里留好钩子而不是开放模板方法本身。6.3 面试手撕三分钟写一个可运行的模板模式面试手撕代码不需要写太复杂的业务。我一般建议用“做任务”这种极简模型abstract class Task { public final void run() { step1(); step2(); step3(); } private void step1() { System.out.println(公共步骤1); } protected abstract void step2(); private void step3() { System.out.println(公共步骤3); } } class MyTask extends Task { Override protected void step2() { System.out.println(子类实现的步骤2); } }写的时候嘴里要讲清楚模板方法是run()抽象方法是step2()父类流程固定子类填空。代码简单但核心点都覆盖了面试官很容易看出你懂行。6.4 项目实战题怎么向面试官讲你用过的模板模式面试官问“你项目里哪里用了模板模式”这种问题是最能拉开差距的。回答的时候不要只说“我用了模板模式”要讲清楚场景、抽象过程、效果。参考话术“我之前重构过一个数据导出模块有订单导出、用户导出、商品导出三个流程都是参数校验、查询数据、转CSV、写文件区别只在查询和转换。我用模板模式建了一个AbstractDataExporter父类把校验和写文件封装成公共方法查询和转换做成抽象方法每种导出只写自己的查询和转换逻辑。重构之后新增一个导出类型只需要实现两个方法大概二十行代码公共流程完全不用动后来我们还加了钩子方法让某些导出可以跳过校验。”这样回答有场景、有细节、有结果比背一百遍概念都有用。面试八股文的本质不是让你死记硬背而是看你有没有真的理解透。模板模式这个概念本身不难难的是在实际项目里用得身法准确。我在实际项目里体会最深的一点是写模板模式的时候最好的状态不是你定义了多复杂的抽象层而是后来有人要扩展一个新需求时他只需要看几个抽象方法的名字和注释就能把业务代码填进去不用去翻父类流程。什么时候做到这个状态说明你的模板方法设计到位了。