ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Java设计模式期末试题全解析:单例、工厂、代理、策略等核心考点精讲

Java设计模式期末试题全解析:单例、工厂、代理、策略等核心考点精讲 简介这份《JAVA设计模式》期末试题A卷归纳版PDF面向高校软件工程、计算机相关专业学生及设计模式初学者可作为期末备考、知识自测与教学参考。试题分为选择题、填空题、名词解释和综合问答四个部分覆盖单一职责、开闭原则、依赖倒置、迪米特法则等核心设计原则并系统考查了工厂、抽象工厂、单例、装饰、备忘录、职责链、组合、解释器等模式的原理与应用场景同时借助Java异常处理、跨平台实现、组件扩展等实际案例帮助读者在具体场景中理解设计模式的价值综合问答部分还注重引导思考模式在跨平台架构与组合结构中的实际应用。资源包共1个PDF文件大小仅53KB内容精炼而考点集中适合考前快速浏览与地毯式复习。目前已有178人学习下载是一份轻量实用的设计模式复习资料既能检验知识掌握程度又能对照答案解析查漏补缺尤其适合期末冲刺阶段快速巩固高频考点。1. 一份期末题背后的知识密度比你想的大得多看到《JAVA设计模式》期末试题这份标题多数人的第一反应是“学生时代的东西”。但换到从业者的视角一份期末题其实是极好的知识体检表——26 个字母能覆盖的设计模式只有 23 个常考而考试只用一个学期就把它们全部纳入考察范围。这在真实的面试八股文场景里是比 LeetCode 更贴近工程实践的一类问题工厂和抽象工厂的区别、观察者和发布订阅的边界、单例在并发下的线程安全这些题目恰好就是你在代码评审里会争执半天的话题。期末试题的典型结构通常是“概念辨析 代码填空 阅读程序写模式 场景设计题”对应的是四个层次的能力背得出定义、看得懂意图、写得出骨架、面对需求能选型。这篇文章不会去逐题“押题”而是把这个结构拆开把每类题目背后真正被考察的工程判断讲清楚。我会用列的题目形式模拟这张考卷的常见布局同时把每一类题目的答题思路和代码级细节展开你可以直接照着在自己的工程里复现一遍。目标就一个考完之后你带走的不只是一张成绩单而是能落地到项目里的设计判断。这套东西在今天的 Java 面试题里依然占据固定席位因为设计模式从来不是知识本身而是代码演进的映射规则。下面我们按试卷的题型顺序从最硬核的创建类模式开始先把“造对象”这件事挖透。2. 创建型模式的 5 个考点单例、工厂、建造者、原型必须手写创建型模式在期末题里通常占 25 到 30 分题型集中在“改错”和“代码填空”。这个板块考察的并不是“你知道有哪几种创建型模式”而是“你能不能识别出代码里违背了什么创建原则”以及“能不能在给定的骨架里补全模式的必要部分”。2.1 单例模式的两种必会写法以及考点暗藏的坑单例几乎是每次必考。传统题面是“给出一个懒汉式单例请指出存在的问题并改正”这就是经典的线程安全改错题。你先记住一个结论双重检查锁DCL必须配合volatile使用否则在 JDK 1.5 以前的版本里由于指令重排第二个线程可能拿到一个尚未完成构造的对象。虽然今天的 JDK 8 基本没有这个问题但面试官和期末题考察的是你“知不知道为什么要有 volatile”。public class SingletonDCL { // volatile 防止指令重排保证 instance 的可见性与有序性 private static volatile SingletonDCL instance; private SingletonDCL() { // 私有构造禁止外部 new } public static SingletonDCL getInstance() { if (instance null) { // 第一次检查避免无谓的同步等待 synchronized (SingletonDCL.class) { // 锁定类对象保证只有一个线程进入 if (instance null) { // 第二次检查防止重复创建 instance new SingletonDCL(); } } } return instance; } }注意看第一次判空在同步块外面这是为了性能——大多数并发进来时不需要争锁直接返回已存在实例。第二次判空在锁里这才是真正兜底的线程安全保证。而volatile则负责解决instance new SingletonDCL()这一行的重排风险因为这一步其实分了三步分配内存空间、初始化对象、将引用指向内存。如果不加volatile第三步可能先于第二步导致另一个线程在第一次判空时看到非 null 的引用却拿到一个还没初始化完的半成品对象。枚举式单例也是期末偏爱的一个考点因为它在反序列化和反射场景下天然免疫攻击。Joshua Bloch 在 Effective Java 里明确推荐过它public enum SingletonEnum { INSTANCE; public void doSomething() { // 业务逻辑 } }这段代码之所以值得写进答案是因为它同时解决了三个问题线程安全枚举类的初始化是天然的、序列化安全枚举类实现序列化时JVM 会自动保证单例不会被破坏的机制、反射安全反射无法通过newInstance创建枚举对象。你如果在答题时把这三个点写全基本就是满分。2.2 工厂模式的进化链条简单工厂、工厂方法、抽象工厂这个是期末考试最容易出大题的板块通常以“请设计一个日志系统支持文件日志和控制台日志的切换”来切入。你首先要分清三者的关系简单工厂是一个静态方法里 switch 类型不是一个完整模式的形态工厂方法把创建动作下沉到子类抽象工厂则面对的是产品族的创建。工厂方法模式有一个非常典型的代码骨架考察点是“你能不能分清楚创建者角色和产品角色”// 产品接口定义日志的统一内容 interface Logger { void log(String message); } // 文件日志的具体实现 class FileLogger implements Logger { public void log(String message) { System.out.println(写入文件: message); } } // 控制台日志的具体实现 class ConsoleLogger implements Logger { public void log(String message) { System.out.println(控制台输出: message); } } // 创建者抽象类把创建动作推迟给子类去做 abstract class LoggerFactory { public void writeLog(String message) { Logger logger createLogger(); logger.log(message); } // 这个抽象方法就是“工厂方法”留给子类决定造什么对象 protected abstract Logger createLogger(); } class FileLoggerFactory extends LoggerFactory { protected Logger createLogger() { return new FileLogger(); } }注意这段代码的一个关键设计LoggerFactory.writeLog是模板方法而createLogger是工厂方法两者组合在一起才是完整的工厂方法模式。很多期末题会在填空里故意把writeLog和createLogger的位置互换考察你是不是真的理解了“稳定业务逻辑放在父类变化的对象创建放在子类”这一核心原则。抽象工厂在此基础上多了一层“产品族”概念区别在于一个工厂生产的是配套的一系列对象。写代码时有个核心规则要记住产品族的横向扩展用抽象工厂产品等级的纵向扩展用工厂方法。如果题目问“新增一个数据库类型比如从 MySQL 换成 Oracle需要改哪儿”答案是只需新增一个实现了抽象工厂接口的具体工厂类不需要改动已有工厂代码这就是开闭原则的完美体现。2.3 建造者模式和原型模式的考点形态建造者在期末题里不会让你默写完整代码而是给你一个构造参数多达 7 个以上的对象问你怎么优雅地创建它。解题关键是理解“链式调用”和“不可变对象”的组合。一个标准的建造者内部有一个 static 的 Builder 类每个设置方法返回this最后调用build()生成目标对象。要注意的坑是Builder 类必须设为 static否则外部无法直接new Builder()使用。原型模式的考点则非常固定Cloneable接口配合clone()方法。题目常用“浅拷贝和深拷贝的区别”来拉开分数。浅拷贝只拷贝数组引用两个对象共享同一块内存修改一个会牵动另一个深拷贝则需要你手动重写clone()方法在里面为引用类型字段也调用一次clone()或者用序列化方式实现。考点通常藏在一个需求里你拷贝了一个用户对象改了用户地址原对象的地址也跟着变了这就是典型的浅拷贝副作用。遇到这种题答案里必须写出被修改的引用类型字段以及对应处理。3. 结构型模式的 7 个难点适配器、代理、装饰器的得分策略结构型模式在期末题的比重是 25 到 35 分多为“读代码写模式名称”和“场景设计题”。这个板块拉开差距的不是记忆而是“你在真实项目里见过什么样子”。因为接口不兼容、类结构太臃肿、功能要动态叠加这些在业务代码里几乎每天都在发生只是很多人没意识到那些前人写的模式叫什么名字。3.1 适配器在“新旧系统对接”中的固定解法适配器的标准考法是给你两个不兼容的接口让两个类能协同工作。比如项目现存一套OldLogger实现提供了record(String)方法但新的日志系统统一要求Logger.log(String level, String message)双参数你需要在不动旧代码的前提下把旧实现接进来。代码答案就是写一个适配器类// 旧系统里的日志实现只能记录单参数消息 class OldLoggerAdapter implements NewLogger { private OldLogger oldLogger; public OldLoggerAdapter(OldLogger oldLogger) { this.oldLogger oldLogger; } // 适配核心把 NewLogger 的调用转换成 OldLogger 的动作 public void log(String level, String message) { oldLogger.record([ level ] message); } }这里的核心洞察是继承和组合的选择。很多学生答案会写成class OldLoggerAdapter extends OldLogger implements NewLogger这是错误示范因为继承会把不属于新接口的额外方法也暴露出去同时把旧类的实现细节强耦合到适配器上。正确的做法是持有旧对象的引用组合只实现新接口把方法调用翻译成旧对象的动作。答题时把这个选择说出来比只写代码更容易拿高分因为期末题目通常会附加一问“为什么适配器不用继承实现”。3.2 代理模式的静态和动态考点必然涉及 JDK 动态代理代理模式的期末大题经常和 Spring AOP 绑在一起考题面一般是“给定一个目标类和一个横切逻辑请用动态代理实现方法增强”。这是全卷最容易暴露水平的一道题因为它要求你会写真正的InvocationHandler而不是只会背定义。import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface UserService { void addUser(String name); } class UserServiceImpl implements UserService { public void addUser(String name) { System.out.println(添加用户: name); } } // 核心代理处理器所有方法的调用都会经过这里统一增强 class LogInvocationHandler implements InvocationHandler { private Object target; public LogInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(操作前: 记录日志参数 (args null ? 无 : args[0])); Object result method.invoke(target, args); // 通过反射调用目标方法 System.out.println(操作后: 日志完成); return result; } } // 客户端创建代理对象注意必须是接口类型引用 UserService proxyService (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new LogInvocationHandler(new UserServiceImpl()) ); proxyService.addUser(张三);你需要注意两个权重很高的考察点。第一个是Proxy.newProxyInstance的三个参数含义分别是目标类的类加载器用来定义代理类、需要代理的接口数组、增强逻辑的处理器实例。第二个是“JDK 动态代理为什么要求目标必须实现接口”这个经典追问这是因为 JDK 动态代理生成的代理类默认继承Proxy类而 Java 是单继承给定一个非接口的类就没法通过实现接口来代理只能借助 CGLIB 通过继承目标类的方式实现。3.3 装饰器最容易和代理混淆的三个判断标准装饰器模式的期末题目通常是给一段输入输出流相关的代码问你这是哪种模式。Java 中BufferedReader(new FileReader(file))这个写法几乎被当作标准考例来用。为什么是装饰器而不是适配器判断标准有这些装饰器与被装饰者实现的是同一个接口或抽象类两者的类型是同族的强调“包裹”与“叠加”适配器则是转换接口两者接口不同。装饰器模式的核心在于动态增强能力它和代理的关键区分点在于装饰器侧重功能增强——你可以外面套一个加密流、再套一个压缩流而代理侧重访问控制——调用者并不知道代理对象的存在。答题时你只要表述清楚三条就稳了装饰器和被装饰者实现的是同一个接口装饰器持有被装饰者的引用通过构造注入装饰器的“装饰”体现在调用前后增加行为目标还是那个目标。3.4 外观模式和组合模式送分题只要记住适用判断外观模式在题目中的识别信号非常固定——出现“提供一个统一的入口”、“让客户端不需要知道子系统内部的调用关系”等表述。代码形态就是一个门面类内部持有多个子系统的引用提供几个公开方法把子系统的调用串起来。组合模式的识别信号则是“树形结构”和“部分与整体的关系”。期末题爱用文件系统做案例目录里包含文件也可以包含子目录两者对外都提供 display 方法。一个重要考点是透明安全性的权衡如果你把管理子节点的方法放在抽象类上透明性叶子节点就必须抛异常或空实现如果你放在容器类上安全性客户端就需要做 instanceof 判断来向下转型。你在答题时需要把它作为一个取舍点来分析而不是单纯写代码。4. 行为型模式的 11 个陷阱重点考策略、观察者、模板方法行为型模式在整张卷子里占分最重约 40 分因为它最能体现“面向对象的本质是多态调度”。期末大题在这部分通常出 2 道设计题多数是“不同角色不同行为”或“多个对象联动”这样的场景。题面看着平凡但得分差距都藏在细节里。4.1 策略模式的必写要点算法的封装与替换实习场景题的经典考法是订单系统里不同的会员等级享受不同的折扣普通会员 95 折、黄金会员 8 折、钻石会员 6 折。要求“便于后续新增等级且不修改已有代码”。标准答案就是策略模式// 策略接口所有折扣算法的统一抽象 interface DiscountStrategy { double calculate(double price); } // 普通会员策略 class NormalDiscount implements DiscountStrategy { public double calculate(double price) { return price * 0.95; } } // VIP 策略高阶用户可以拿到更低折扣 class VipDiscount implements DiscountStrategy { public double calculate(double price) { return price * 0.8; } } // 上下文类持有当前策略供外部切换不关心具体算法细节 class OrderContext { private DiscountStrategy strategy; public OrderContext(DiscountStrategy strategy) { this.strategy strategy; } // 动态切换策略 public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } public double finalPrice(double price) { return strategy.calculate(price); } }这个代码的考察重点有三个。第一策略接口的定义是否清晰——只有一个方法没有任何多余的东西。第二上下文类是否通过构造或 setter 注入了策略对象——它不能直接 new 一个具体策略。第三新增策略时是否不需要改动任何已有类——这就是开闭原则的直接验证方式。很多人在真实项目里用 if-else 堆接口实现面试里被问“怎么改造重构”时这套答案就是那块垫脚石。4.2 观察者模式的实现要点和发布订阅的区别观察者模式的期末题目经典形态是天气站实时发布气象数据多个展示面板自动更新做到数据一变所有依赖者都同步变化。代码要点是“被观察者持有观察者集合通知方法里循环调用”import java.util.ArrayList; import java.util.List; // 观察者接口所有展示面板都实现它 interface Observer { void update(float temperature); } // 被观察者主题 class WeatherData { private ListObserver observers new ArrayList(); private float temperature; // 注册一个新的观察者 public void attach(Observer observer) { observers.add(observer); } // 从集合移除观察者 public void detach(Observer observer) { observers.remove(observer); } // 数据变更后通知所有观察者的标准写法 public void notifyObservers() { for (Observer observer : observers) { observer.update(temperature); } } // 外部调用此方法推送新的测量数据 public void setTemperature(float temperature) { this.temperature temperature; notifyObservers(); } }这段代码里有两个很关键的隐含考点。第一点是顺序依赖问题notifyObservers必须写在setTemperature的数据变更之后如果在赋值前通知所有观察者拿到的都是旧数据。第二点是观察者更新时的异常处理真实项目里一个观察者抛异常会中断整个通知链路考试虽然不考但如果你在答题时顺带提一句“真实场景里循环通知应该包 try-catch 隔离异常”阅卷老师会认为你真有工程经验。观察者和“发布订阅模式”的区别也常被拿来吊考生观察者模式是“被观察者直接持有观察者引用”耦合是一次通话的发布订阅模式中间多了一层消息队列双方互不认识。这个区分在 Spring Context 里体现得尤为明显值得你留意。4.3 模板方法体现“反向控制”的必考框架模板方法的题面一般是“泡茶和泡咖啡有固定步骤有的步骤相同有的步骤不同”要求用继承来复用算法骨架。核心洞察是父类写死算法的稳定骨架具体步骤的实现延迟到子类去完成。// 模板方法父类定义完整流程骨架 abstract class BeverageTemplate { // 这个方法就是模板方法定义了不可变的流程顺序 public final void makeBeverage() { boilWater(); brew(); pourInCup(); if (needCondiment()) { addCondiment(); } } protected void boilWater() { System.out.println(烧水); } protected void pourInCup() { System.out.println(倒入杯中); } // 抽象方法泡什么需要子类决定 protected abstract void brew(); protected abstract void addCondiment(); // 钩子方法子类可以通过覆写控制某一步是否执行 protected boolean needCondiment() { return true; } }考点非常清晰“final”关键字的作用是什么为什么模板方法必须设为 final钩子方法needCondiment()的用途是什么。这两个问题回答方向是模板方法设 final 是为了防止子类重写步骤顺序、破坏算法骨架而钩子方法的存在是为了把流程控制权部分交还给子类这就是好莱坞原则不要调用我们我们会调用你的体现。列表与代码之外的考察形式期末题里还有一种“给一段含大量 if-else 的代码请你选择一种模式重构它”这类题的常见答案就是策略模式算法选择或状态模式状态流转你要注意从题目情境去辨别差异策略模式侧重算法的无状态替换状态模式侧重状态驱动的行为变化以及状态之间已经定义好的转移条件。5. 答题和读题的关键区分面向对象设计原则在判分词里的权重判断题和概念辨析题在整个试卷里通常是 15 分左右但它们对总分的撬动并不只是分值那么简单——阅卷老师看主观大题时最先扫的是你的语句里有没有出现“开闭原则”“依赖倒置”“接口隔离”这些判分词。与其说这是考原则不如说是在检测你的“模式语言”是否成型。你需要能清楚区分里氏替换和依赖倒置这两个最容易混淆的原则然后在答题时准确地使用它们。里氏替换原则指的是子类对象替换父类对象后程序行为不发生变化——它管的是继承关系的正确性。依赖倒置原则指的是高层模块不依赖底层模块两者都依赖抽象抽象不依赖细节细节依赖抽象——它管的是模块之间的方向问题。很多代码设计题的标准答案表述里会同时出现两者用接口隔离变化这叫依赖倒置保证新加的子类能替换父类而不出问题这叫里氏替换。你写答案时按对应关系写词会显得答题认真且到位。期末考试里的设计题往往还藏着“平衡”的评分点比如一道题说“某模块需要同时支持两种不相关的行为扩展”标准回答里如果只给出一种模式通常会给一部分分数但如果你能附加说明“该场景其实也能用装饰器但考虑题意更偏向策略”那就能拿到满分档的额外印象分。6. 考前抢救用“模式识别树”做最后一遍知识遍历如果你只剩一个晚上的复习时间不要按 23 个模式的顺序过应该用模式识别树来做分类记忆。这个方法对面试同样适用面对一段代码你先判断它解决的是对象创建、结构组合还是行为调度再往下走判断具体的模式。我个人的判断流程是这样的看到代码中出现了私有构造方法且对外提供一个静态方法先想到单例看到抽象类和子类同时出现且父类方法调用抽象方法先想到模板方法看到构造函数里传入了多个接口对象先想到策略或外观看到目标对象并不是原始对象而是被包装了一层先想到代理或装饰器再通过是否实现了相同接口区分。给一个集成的代码级练习作为最终验证你来写一个含有策略和观察者组合的迷你场景一个购物车价格变化后通知所有营销组件组件针对不同等级用户用不同策略计算优惠。// 价格变化事件监听 interface PriceListener { void onPriceChanged(double newPrice); } // 营销通知组件 class PromotionNotifier implements PriceListener { private DiscountStrategy strategy; public PromotionNotifier(DiscountStrategy strategy) { this.strategy strategy; } public void onPriceChanged(double newPrice) { System.out.println(优惠后价格: strategy.calculate(newPrice)); } } // 购物车被观察者 class ShoppingCart { private ListPriceListener listeners new ArrayList(); private double price; public void attach(PriceListener l) { listeners.add(l); } public void setPrice(double price) { this.price price; for (PriceListener listener : listeners) { listener.onPriceChanged(price); } } }把这个代码在本地跑一遍就算你用一句话写全购物车类也无妨重点是验证一件事观察者在setPrice里循环调用的位置、策略在PromotionNotifier里通过构造注入了接口。你如果能一眼看出这套组合的职责边界那你面对期末题和 Java 面试八股文里的高频设计题都有足够的底气往下写。最后再补一句实在话设计模式不是靠背出来的而是靠对比出来的——每次看完一个模式的类和代码头脑里过一遍“如果我不用这个模式用别的会有什么代价”这样才能让这张卷子里的分数真正变成手里的能力。本文还有配套的精品资源点击获取
返回列表