设计模式常见面试题总结
[](https://javaguide.cn/about-the-au
前言 这是 JavaGuide 面试突击版本 ,只保留最常问的面试题,并对重点内容进行了 ⭐️ 标注。提供亮色和暗色两个主题,需要打印的朋友请选择亮色版本。 时间充裕的朋友,推荐使用 JavaGuide 网站系统学习,内容更全面深入。 如果你想要付费支持/面试辅导(比如简历优化、一对一提问、高频考点突击资料等)的话,欢迎了解我的知识星球。已经坚持维护六年,内容持续更新,虽白菜价(0.4元/天)但质量很高,主打一个良心! 面试突击最新版可在公众号回复「 PDF 」获取(知识星球会提前同步最新版)。 重要说明 本站所有面试题保持 年度系统性优化完善 ,严格同步 Java 技术生态与招聘市场的最新动态, 确保内容时效性与前瞻性 。 软件设计原则有哪些? 在软件开发中,我们遵循一系列面向对象设计原则来提升代码的可维护性、可复用性和可扩展性,从而构建出更加健壮和灵活的系统。这些原则中最核心、最著名的就是 SOLID 原则,同时还有几个非常重要的补充原则。 SOLID 原则 : 1. 单一职责原则 (Single Responsibility Principle, SRP) 核心思想 :一个类或模块应该有且只有一个引起它变化的原因。 通俗来说 :一个类只做一件相关的事情。如果一个类承担了多个职责,那么当其中一个职责发生变化时,可能会影响到其他职责的实现,这会增加代码的复杂性和耦合度。 2. 开闭原则 (Open Closed Principle, OCP) 核心思想 :软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。 通俗来说 :当需要增加新功能时,我们应该通过增加新的代码(例如新的子类或实现类)来实现,而不是去修改已有的、工作良好的代码。这是面向对象设计中最重要的一条原则,是实现系统可复用性和可维护性的基石。 3. 里氏替换原则 (Liskov Substitution Principle, LSP) 核心思想 :所有引用基类的地方,都必须能够透明地使用其子类的对象。 通俗来说 :子类应当可以完全替换父类,并且程序的行为不会产生任何错误或异常。这意味着子类在继承父类时,不应该改变父类预期的行为。它是实现开闭原则的重要方式之一。 4. 接口隔离原则 (Interface Segregation Principle, ISP) 核心思想 :客户端不应该被强迫依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口之上。 通俗来说 :接口的设计要“专”,不要设计大而全的“胖接口”,而应该根据功能设计多个细粒度的接口。这样可以避免一个类为了实现接口中的某一个功能,而不得不实现所有其他它并不需要的功能。 5. 依赖倒置原则 (Dependency Inversion Principle, DIP) 核心思想 :高层模块不应该依赖于低层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。 通俗来说 :我们要面向接口编程,而不是面向实现编程。代码的依赖关系应该通过抽象(如接口或抽象类)来建立,而不是具体的实现类。这样可以大大降低模块间的耦合度,方便系统的升级和扩展。 其他重要原则 : 1. 合成/聚合复用原则 (Composite/Aggregate Reuse Principle, CARP) 核心思想 :尽量使用对象组合(合成/聚合)的方式,而不是继承来达到代码复用的目的。 通俗来说 :“has a” 关系通常比 “is a” 关系更灵活。继承是一种强耦合关系(白盒复用),而组合是一种弱耦合关系(黑盒复用),后者更容易维护和扩展。 2. 迪米特法则 (Law of Demeter, LoD) / 最少知识原则 (Least Knowledge Principle, LKP) 核心思想 :一个软件实体应当尽可能少地与其他实体发生相互作用。 通俗来说 :一个对象应该对其他对象有尽可能少的了解,即“只和你的直接朋友交谈”。这有助于降低类之间的耦合度,让每个模块更加独立。 这些原则并非孤立存在,而是相辅相成,共同指导我们写出“高内聚、低耦合”的优质代码。 什么是设计模式? 设计模式(Design Pattern)可以理解为一套 在特定场景下,针对软件设计中常见问题的、可复用的解决方案 。 它不是一个可以直接转换成代码的最终设计,而更像一个 蓝图或模板 ,描述了如何组织类和对象来解决某个特定的设计难题。 我们可以从以下几个角度来理解它: 1. 它是经验的总结 :设计模式是无数软件工程师在长期实践中,经过反复试验和验证,总结出的一套行之有效的“最佳实践”。 2. 它是沟通的语言 :设计模式为开发者提供了一套通用的词汇。当你说“这里我用了一个工厂模式”,团队里的其他成员能立刻理解你的设计意图,这极大地提高了沟通效率。 3. 它解决了什么问题 :它的核心目标是提升代码的 可重用性 、 可读性 、 健壮性 和 可维护性 。通过遵循设计模式,我们可以写出结构更清晰、耦合度更低、更容易扩展和修改的代码,避免重复“造轮子”和踩一些常见的“坑”。 总而言之,设计模式是软件工程化的重要基石,它帮助我们将代码设计从“个人技艺”提升到“工程规范”的层面。 设计模式的分类了解吗? 根据设计模式的目的和关注点的不同,它们通常被分为三大类,这是由《设计模式:可复用面向对象软件的基础》这本书(常被称为 "GoF 四人帮" 的著作)提出的经典分类: 1. 创建型模式 (Creational Patterns) 关注点 : 对象的创建过程 。这类模式将对象的创建和使用解耦,使得程序在创建对象时更具灵活性。它们隐藏了对象创建的复杂逻辑,使得我们无需直接使用 new 关键字。 核心思想 :提供一种机制,使得客户端代码不必关心它所需要的具体是哪个类的实例,也不必关心这些实例是如何被创建和组织的。 常见模式 : 单例模式 (Singleton) 工厂方法模式 (Factory Method) 抽象工厂模式 (Abstract Factory) 建造者模式 (Builder) 原型模式 (Prototype) 2. 结构型模式 (Structural Patterns) 关注点 : 类和对象的组合 。这类模式研究如何将类和对象组合在一起,形成更大、更复杂的结构,同时保持结构的灵活性和效率。 核心思想 :通过继承、组合等方式,在不改变原有类的情况下,为其增加新的功能或适配不同的接口。 常见模式 : 适配器模式 (Adapter) 桥接模式 (Bridge) 组合模式 (Composite) 装饰器模式 (Decorator) 外观模式 (Facade) 享元模式 (Flyweight) 代理模式 (Proxy) 3. 行为型模式 (Behavioral Patterns) 关注点 : 对象之间的通信和职责分配 。这类模式专门处理对象之间的交互和协作,以及算法和责任的分配。 核心思想 :通过定义对象间的通信模式来降低它们之间的耦合度,使得系统中的对象可以独立地变化。 常见模式 : 责任链模式 (Chain of Responsibility) 命令模式 (Command) 迭代器模式 (Iterator) 中介者模式 (Mediator) 备忘录模式 (Memento) 观察者模式 (Observer) 状态模式 (State) 策略模式 (Strategy) 模板方法模式 (Template Method) 访问者模式 (Visitor) 解释器模式 (Interpreter) 工厂模式 请谈一谈你对工厂模式的理解 工厂模式是创建型设计模式中最常用的一族,它主要解决的是 对象的创建问题 ,将对象的创建和使用过程进行解耦。根据其复杂度和应用场景的不同,通常分为三种: 1. 简单工厂模式 2. 工厂方法模式 3. 抽象工厂模式 说一说简单工厂模式 简单工厂模式指由一个 单一的工厂对象 来创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。 UML 类图如下: 角色组成 : 工厂 (Factory) :负责实现创建所有实例的内部逻辑。 抽象产品 (AbstractProduct) :所有被创建对象的父类或接口。 具体产品 (ConcreteProduct) :工厂类创建的目标实例。 解决的问题 :将客户端代码与具体产品的实现类解耦。客户端只需“消费”产品,而无需关心产品是如何被创建的。 优缺点 : 优点 :结构简单,将创建逻辑集中管理,实现了职责分离。 缺点 : 违反了开闭原则 。当需要增加新产品时,必须修改工厂类内部的判断逻辑(例如 if else 或 switch 语句),这使得工厂类越来越臃肿,难以维护。 实例 : Java 的 方法,内部会根据时区、地区等参数创建具体的日历对象。 Spring 的 在概念上就是一个强大的简单工厂,你通过一个 bean 的名字(ID)就能获取到对应的实例,而无需关心这个 bean 是如何被创建和配置的。 简单工厂代码实现 : 输出结果: 静态工厂和简单工厂的区别 静态工厂(Static Factory) 是一种基于 类的静态方法 创建对象的方式,属于《Effective Java》中推荐的创建对象的技巧(严格来说不算标准设计模式,但常被归类为创建型模式)。 上面的简单工厂模式代码用静态工厂改写如下: 静态工厂和简单工厂对比如下: | 维度 | 静态工厂(Static Factory) | 简单工厂(Simple Factory) | | | | | | 工厂位置 | 工厂方法以 方法形式内嵌在 产品类或工具类 中 | 工厂逻辑独立在 单独的工厂类 中 | | 产品范围 | 通常用于创建 同一类产品 (或其变体) | 用于创建 多个不同类型但相关的产品 (如同一接口的不同实现) | | 参数依赖 | 通常用 不同方法名 区分产品,可零参数 | 必须传 参数 (字符串/枚举等)指定产品类型 | | 扩展性 | 新增产品需修改包含静态方法的类,违反 OCP | 新增产品需修改工厂类判断逻辑,同样违反 OCP | | 核心优势 | API 语义清晰(如 ),调用直观 | 创建逻辑集中管理,客户端无需关心具体实现 | NIO 中大量用到了工厂模式,比如 类的 方法用于创建 对象(静态工厂)、 类的 方法创建 对象(静态工厂)、 类( 包下的类,属于 相关的一些内部实现)的 的方法创建 对象(简单工厂)。可以顺带阅读一下笔者写的这篇 Java IO 设计模式总结。 工厂方法模式了解吗? 为了解决简单工厂模式违反开闭原则的问题,工厂方法模式将创建具体对象的任务 下放给子类 。它定义了一个用于创建对象的抽象方法,但由子类来决定要实例化哪一个类。 UML 类图如下: 角色组成 : 抽象工厂 (AbstractFactory) :声明了创建产品的抽象方法。 具体工厂 (ConcreteFactory) :实现抽象方法,负责创建具体的产品。 抽象产品 (AbstractProduct) :与简单工厂模式相同。 具体产品 (ConcreteProduct) :与简单工厂模式相同。 解决的问题 :在不修改现有工厂类的情况下,轻松 扩展新的产品 。如果想增加一个新产品,只需增加一个新的具体产品类和一个对应的具体工厂类即可,完全符合 开闭原则 。 优缺点 : 优点 :完美遵循开闭原则,扩展性好。创建逻辑被分散到各个具体工厂中,符合单一职责原则。 缺点 :每增加一个产品,就需要增加一个具体工厂类,这会导致系统中的 类数量成倍增加 ,增加了系统的复杂性。 实例 :Java 集合框架中 接口的 方法。 和 都实现了这个接口,但它们各自的 方法返回的是不同的迭代器实现类 ( 和 ),这就是典型的工厂方法模式。 工厂方法代码实现 : 输出结果: 抽象工厂模式了解吗? 当需要创建的 不是单一产品,而是一个产品族(一系列相互关联或相互依赖的对象) 时,就该使用抽象工厂模式。它提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。可以理解为它是“ 工厂的工厂 ”。 "产品族"和 "产品等级结构"概念介绍 : 产品等级结构:同一类产品的不同实现(如按钮可以有 Windows 按钮、Mac 按钮,这构成一个产品等级结构) 产品族:同一品牌或风格下的不同产品(如 Windows 风格下的按钮、文本框、复选框,构成一个产品族) UML 类图如下: 角色组成 : 抽象工厂 (AbstractFactory) :定义了创建一系列不同产品(一个产品族)的接口。 具体工厂 (ConcreteFactory) :实现接口,创建特定主题或风格的产品族。 抽象产品 (AbstractProduct) :为产品族中的每一种产品定义接口。 具体产品 (ConcreteProduct) :实现抽象产品接口,是具体工厂创建的目标。 解决的问题 :解决创建 一整个产品家族 的问题,保证客户端在切换不同产品族时,能获得一整套相互兼容和匹配的对象。 优缺点 : 优点 :非常适合用于创建一系列相互匹配的产品。切换整个产品族非常方便,只需更换具体的工厂即可。 缺点 : 扩展新的产品等级结构困难 。例如,如果产品族需要增加一个“鼠标”,那么 接口就需要增加一个 方法,所有已经实现的具体工厂类也都要跟着修改,这违反了开闭原则。 实例 : 最典型的例子是更换软件皮肤。一个 接口可以定义 、 等方法。然后可以有 和 两个具体工厂,分别用来创建一套 Windows 风格或 Mac 风格的 UI 组件。 JDBC 也是一个很好的例子。 、 、 就可以看作一个产品族。不同的数据库驱动(如 MySQL Driver、Oracle Driver)就扮演了具体工厂的角色,负责创建对应数据库的连接和操作对象。 抽象工厂代码实现 : 输出结果: 简单工厂、工厂方法和抽象工厂模式的区别 | 特性 | 简单工厂模式 | 工厂方法模式 | 抽象工厂模式 | | | | | | | 复杂度 | 低 | 中 | 高 | | 关注点 | 创建 单一类型 的产品 | 创建 单一类型 的产品 | 创建 一族相关联 的产品 | | 开闭原则 | 违反 | 遵守 | 对扩展产品族遵守,对扩展产品类型违反 | | 核心 | 一个集中式工厂 | 将创建逻辑延迟到子类 | 创建产品家族 | 单例模式 什么是单例模式?有什么优点? 单例模式属于创建型模式,一个单例类在任何情况下都只存在一个实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。 优缺点 : 优点 : 节省资源 :由于内存中只有一个实例,减少了频繁创建和销毁对象带来的性能开销。 全局控制 :方便对唯一实例进行统一管理,避免对共享资源的多重占用。 缺点 : 扩展性差 :因为没有抽象层,难以扩展。 职责过重 :一个类既要负责自身的业务逻辑,又要负责保证单例,有点违背单一职责原则。 测试困难 :全局状态在单元测试中可能引入依赖和副作用。 单例模式的常见写法有哪些? 单例模式的实现方式有很多种,它们主要在 线程安全 和 懒加载 (Lazy Loading)这两个维度上有所不同。 1. 饿汉式 (Eager Initialization) 线程安全 饿汉式单例模式,顾名思义,类一加载就创建对象,这种方式比较常用,但容易产生垃圾对象,浪费内存空间。 饿汉式单例是如何保证线程安全的呢? 它是基于类加载机制避免了多线程的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例。 代码实现,以及使用反射破坏单例: 使用反射破坏单例,代码如下: 输出结果如下: 优缺点 : 优点 :实现简单,天生线程安全,执行效率高。 缺点 :不是懒加载。如果这个实例从未使用过,会造成内存浪费。 懒加载 (lazy loading):使用的时候再创建对象 2. 懒汉式 (Lazy Initialization) 线程不安全 为了解决饿汉式的资源浪费问题,懒汉式在第一次被使用时才创建实例。但下面这种基础写法是 线程不安全 的。 使用多线程破坏单例,测试代码如下: 输出结果如下: 优缺点 : 优点 :实现了懒加载。 缺点 :线程不安全。在多线程环境下,可能创建出多个实例。 3. 懒汉式 同步方法 懒汉式单例如何保证线程安全呢? 通过 关键字加锁保证线程安全, 可以添加在方法上面,也可以添加在代码块上面,这里演示添加在方法上面,存在的问题是每一次调用 获取实例时都需要加锁和释放锁,这样是非常影响性能的。 4. 懒汉式 双重检查锁 双重检查锁 (Double Checked Locking, DCL) 是对同步方法懒汉式的优化,它通过两次 null 检查和 代码块,在保证线程安全的同时,大大提升了性能。 双重检查的必要性 : 外层检查 :当实例已创建时,所有线程可直接返回结果,避免不必要的同步开销,这是性能优化的关键。 内层检查 :解决并发竞争问题 —— 当多个线程同时通过外层检查时,同步块保证只有一个线程进入创建逻辑,后续线程会被内层检查拦截,确保仅创建一个实例。 volatile 关键字的作用 : 采用 关键字修饰也是很有必要的, 这段代码其实是分为三步执行: 1. 为 分配内存空间 2. 初始化 3. 将 指向分配的内存地址 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1 3 2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 () 后发现 不为空,因此返回 ,但此时 还未被初始化。 既然聊到了多线程,建议你顺带复习一下这块的知识点,推荐阅读笔者写的这几篇文章: Java并发常见面试题总结(上)(多线程基础知识,例如线程和进程的概念、死锁) Java并发常见面试题总结(中)(各种锁,例如乐观锁和悲观锁、 关键字、 ) Java并发常见面试题总结(下)( 、线程池、 、AQS、虚拟线程等) 优缺点 : 优点 :懒加载、线程安全、性能较高。 缺点 :实现相对复杂,需要正确理解 volatile 的作用 5. 静态内部类 推荐 虽然 DCL 已经很优秀了,但在现代 Java 开发中,我们有更优雅、更推荐的实现方式。 这是我个人比较推荐的写法,它巧妙地利用了 JVM 的类加载机制 来实现懒加载和线程安全。 静态内部类单例是如何实现懒加载的呢? 首先,我们先了解下类的加载时机。 对于初始化阶段,虚拟机严格规范了有且仅有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类): 1. 遇到 、 、 或 这 4 条字节码指令时: : 创建一个类的实例对象。 、 : 读取或设置一个类型的静态字段(被 修饰、已在编译期把结果放入常量池的静态字段除外)。 : 调用类的静态方法。 2. 使用 包的方法对类进行反射调用时如 , 等等。如果类没初始化,需要触发其初始化。 3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。 4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 方法的那个类),虚拟机会先初始化这个类。 5. 和 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 来初始化要调用的类。 6. 「补充,来自issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。 关于类加载的详细介绍可以阅读笔者写的这篇文章:类加载过程详解。 这 6 种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是 " 有且仅有 ",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的情况。 当 方法被调用时, 才在 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 也真正被创建,然后再被 方法返回出去,这点同饿汉模式。 那么 在创建过程中又是如何保证线程安全的呢?在《深入理解 JAVA 虚拟机》中,有这么一句话: 虚拟机会保证一个类的 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 方法,其他线程都需要阻塞等待,直到活动线程执行 方法完毕。如果在一个类的 方法中有耗时很长的操作,就可能造成多个进程阻塞( 需要注意的是,其他线程虽然会被阻塞,但如果执行 方法后,其他线程唤醒之后不会再次进入 方法。同一个加载器下,一个类型只会初始化一次。 ),在实际应用中,这种阻塞往往是很隐蔽的。 从上面的分析可以看出 在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。 6. 枚举单例 推荐 《Effective Java》作者 Joshua Bloch 极力推崇的方式。 枚举在 java 中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例,可以直接通过如下方式调用获取实例: 使用下面的命令反编译枚举类 得到如下内容 从枚举的反编译结果可以看到,INSTANCE 被 修饰,所以可以通过类名直接调用, 并且创建对象的实例是在静态代码块中创建的 ,因为 static 类型的属性会在类被加载之后被初始化,当一个 Java 类第一次被真正使用到的时候静态资源被初始化、Java 类的加载和初始化过程都是线程安全的,所以创建一个 enum 类型是线程安全的。 通过反射破坏枚举,实现代码如下: 运行结果报如下错误: 所以无法通过反射创建枚举的实例。 优点 : 实现极其简单。 天生线程安全,由 JVM 从语言层面保证。 能有效防止通过反射和反序列化来破坏单例。Java 规定,不能通过反射来创建枚举实例,并且在反序列化时,JVM 会特殊处理,保证返回的是同一个枚举实例。 缺点 :不是懒加载。 适配器模式 适配器模式了解吗? 适配器模式(Adapter Pattern)是一种结构型设计模式,它的核心作用是 将一个类的接口转换成客户端所期望的另一个接口 ,从而使原本因接口不兼容而不能在一起工作的类可以协同工作。 你可以把它想象成一个我们日常生活中常用的 电源适配器或转换头 。比如,你的笔记本电脑电源是两脚插头(被适配者 Adaptee),但墙上的插座是三孔的(目标 Target)。这时,你需要一个转换头(适配器 Adapter),它一端能插进三孔插座,另一端能接收你的两脚插头,这样问题就解决了。 适配器模式主要包含三个核心角色: Target (目标接口) :客户端(Client)期望和它直接交互的接口。在上面的例子里,就是那个三孔插座。 Adaptee (被适配者) :已存在的、但接口与 不兼容的类。也就是那个两脚插头。 Adapter (适配器) :模式的核心。它实现了 接口,同时内部包装了一个 类的实例,负责将对 接口的调用转换为对 接口的调用。它就是那个转换头。 优缺点 : 优点: 增强了类的复用性 :可以复用已存在的、功能强大的 类,而无需修改其源码。 提高了灵活性和扩展性 :可以非常方便地替换或增加新的适配器,来适配不同的 ,符合开闭原则。 解耦 :将客户端(Client)与具体的实现类(Adaptee)解耦,客户端只需要和目标接口(Target)打交道。 缺点: 增加了系统的复杂性 :每适配一个类都需要增加一个适配器类,如果过度使用,会导致系统中的类数量增多,代码可读性有所下降。 (针对类适配器) 限制较多 :由于语言的单继承限制,类适配器一次最多只能适配一个 类,并且要求 必须是接口或抽象类。 适配器模式实现方式有哪些?如何选择? 适配器模式主要有两种实现方式: 类适配器 和 对象适配器 。 类适配器 : 原理 :通过 类继承 来实现。 类同时继承 类并实现 接口。 特点 :由于 Java 是单继承,这意味着 只能适配一个 类。它的耦合度相对较高。 对象适配器 : 原理 :通过 对象组合/关联 来实现。 类实现 接口,并在内部持有一个 类的实例。 特点 :这种方式更加灵活,因为 可以适配 的任何子类。它遵循了“合成/聚合复用原则”,是更推荐、更常用 简单对比一下: | 对比维度 | 类适配器 | 对象适配器 | | | | | | 实现原理 | 继承 | 组合/关联 | | 耦合度 | 高(编译时绑定) | 低(运行时绑定) | | 灵活性 | 低,只能适配一个Adaptee类 | 高,可以适配Adaptee及其所有子类 | | 推荐度 | 一般 | 推荐 | 哪些地方用到了适配器模式? : 这个方法就是一个典型的适配器。它把一个数组( )适配成一个 接口( ),让我们可以用操作 的方式去操作一个数组。 中的字符流与字节流转换 : 就是一个适配器。它将一个字节输入流 ( )适配成一个字符输入流 ( ),解决了处理文本文件时字节到字符的转换问题。 也是同理。可以顺带阅读一下笔者写的这篇Java IO 设计模式总结。 日志框架 SLF4J : SLF4J (Simple Logging Facade for Java) 本身是一个日志门面,但它的桥接包(如 slf4j log4j12)就是适配器。它让你的应用程序代码(Client)统一面向 SLF4J 的 API( )编程,而底层可以无缝地切换到 Log4j、Logback 等具体的日志实现( )。 代理模式 什么是代理模式? 代理模式(Proxy Pattern)是一种结构型设计模式。它的核心思想是 为其他对象提供一种代理,以控制对这个对象的访问 。 你可以把它想象成生活中的“明星经纪人”。粉丝(客户端 Client)不能直接联系到明星(真实对象 RealSubject),而是需要通过经纪人(代理 Proxy)。这个经纪人可以帮明星处理很多事情,比如过滤掉不重要的请求、安排日程、谈合同等等,而明星本人则可以专注于自己的核心工作——表演。 在这个过程中,经纪人和明星都实现了相同的“能力接口”(比如“接受采访”、“商业演出”),所以对外界来说,与经纪人沟通和与明星直接沟通,在接口上是一致的。 UML 类图: 代理模式主要包含三个核心角色: Subject (抽象主题) :定义了真实对象和代理对象共同的接口。 RealSubject (真实主题) :被代理的实际对象,它执行业务的核心逻辑。 Proxy (代理) :持有对真实对象的引用,并实现了抽象主题接口。它可以在调用真实对象前后执行额外的操作。这就是“经纪人”。 为什么要使用代理模式? 代理模式的价值在于,它可以在 不改变真实对象代码 的前提下,为其增加额外的功能。这些功能通常是通用的、与核心业务无关的,例如: 1. 远程代理 (Remote Proxy) :为一个位于不同地址空间的对象提供本地的代表。它可以隐藏网络通信的细节,使得客户端调用远程对象就像调用本地对象一样。例如,Dubbo、gRPC 等 RPC 框架的客户端存根(Stub)就是典型的远程代理。 2. 虚拟代理 (Virtual Proxy) :根据需要创建开销很大的对象。如果真实对象创建和初始化非常耗时,虚拟代理可以延迟它的创建,直到客户端真正需要它时才实例化。比如,加载一个高清大图,可以先显示一个占位符(代理),在后台真正加载图片(真实对象)。 3. 保护代理/安全代理 (Protection Proxy) :控制对真实对象的访问权限。代理可以根据调用者的权限,决定是否将请求转发给真实对象。例如,Spring Security 中实现的方法级别的权限控制。 4. 智能引用 (Smart Reference) :在访问对象时执行一些附加操作,如缓存、日志记录、事务管理等。这在 Spring AOP 中体现得淋漓尽致。 代理模式在 Java 中如何实现? 关于静态代理和动态代理的具体实现和区别可以参考笔者写的这篇文章:Java 代理模式详解。 观察者模式 说一说观察者模式 观察者模式是一种非常经典和实用的 行为型设计模式 。它的核心思想在于定义了一种 一对多 的依赖关系:当一个对象(我们称之为“被观察者”或“主题”)的状态发生改变时,所有依赖于它的对象(即“观察者”)都会自动收到通知并进行相应的更新。 这种模式的本质是 解耦 ,它将被观察者和观察者分离开来,使得它们可以独立地变化和复用,而不需要知道对方的具体实现细节。 UML 类图: 观察者模式通常包含四个核心角色: 主题/被观察者 (Subject): 这是一个接口或抽象类,它负责维护一个观察者列表,并提供添加、删除观察者的接口。最关键的是,它还定义了通知所有观察者的 notify() 方法。 具体主题/具体被观察者 (ConcreteSubject): 它是Subject的具体实现。它包含了业务逻辑,并在自身状态发生变化时,调用继承自Subject的 notify() 方法,通知所有已注册的观察者。 观察者 (Observer): 同样是一个接口或抽象类,它定义了一个 update() 方法。当观察者接收到来自主题的通知时,这个方法就会被调用。 具体观察者 (ConcreteObserver): 它是Observer的具体实现。在 update() 方法中,它会根据收到的通知,完成具体的业务逻辑,比如更新自身状态、执行某个操作等。 优缺点 : 优点: 高度解耦: 这是它最大的优点。主题和观察者之间是松耦合的,主题只知道它有一系列观察者,但不需要知道它们是谁、做什么。这为系统带来了极大的灵活性。 符合开闭原则: 系统是“对扩展开放,对修改关闭”的。当需要增加新的响应行为时,我们只需要创建一个新的具体观察者类并注册它即可,完全不需要修改主题的代码。 支持广播通信: 主题可以向所有注册的观察者广播通知,这在很多场景下非常高效。 缺点: 1. 潜在的性能问题: 如果观察者数量非常多,或者某个观察者的 方法逻辑复杂、耗时较长,那么通知过程可能会导致主线程阻塞,影响性能。在某些场景下,可以考虑使用异步通知来优化。 2. 可能导致意外的级联或循环: 如果观察者之间存在复杂的依赖关系,一个观察者的更新可能会触发另一个观察者(它同时也是另一个主题)的更新,形成复杂的调用链。如果设计不当,甚至可能导致循环调用,造成系统崩溃。 3. 调试困难: 由于其松耦合的特性,当程序出现问题时,有时很难追踪到一个事件发生后,具体是哪个观察者的行为导致了问题。 补充一下 :观察者模式在不同框架中可能有不同的命名风格,例如: Java 中的 (主题)和 (观察者); Spring 中的 (事件,对应主题状态)和 (监听器,对应观察者)。 你的项目是怎么用的观察者模式? 在我的项目中,观察者模式得到了广泛应用,一个非常典型的例子就是 支付成功后的业务处理 。 场景是这样的: 在一个电商系统中,当用户支付成功后,我们需要触发一系列独立的后续操作,比如: 更新订单状态为“已支付”。 给用户的账户增加积分。 通知仓储系统准备发货。 给用户发送一封确认邮件。 如果不用设计模式, 我们可能会在支付成功的方法里,把这四个操作串行地写下来。这样做的问题显而易见:支付核心逻辑与各种业务逻辑紧紧地耦合在一起,每次新增一个类似“赠送优惠券”的需求,都必须去修改这个已经很庞大和脆弱的核心方法,这严重违反了开闭原则。 我们的解决方案是: 我们将“支付成功”这个事件抽象为 具体主题 (ConcreteSubject) 。而上述的“更新订单”、“增加积分”、“通知仓库”、“发送邮件”等操作,则分别被设计成独立的 具体观察者 ( ) 。 工作流程是: 在系统初始化时,这些观察者会把自己注册到“支付成功”这个主题上。当支付网关回调我们的接口,确认支付成功后,主题的 方法被调用,然后它会立即执行 ,遍历内部的观察者列表,并依次调用它们的 方法。 这样做的价值在于: 高度解耦: 支付核心流程完全不关心后续有哪些业务,它只负责发布“我成功了”这个消息。 极强的扩展性: 未来如果需要增加“支付成功后赠送一张抽奖券”的新功能,我们只需要新增一个 类,并在系统启动时完成注册即可,原有代码一行都不用动。 装饰器模式 什么是装饰器模式? 装饰器模式是一种 结构型模式 ,它的核心思想是 在不改变原有对象结构和代码的前提下,动态地为该对象添加额外的功能 。 它就像给一个物体“穿衣服”,你可以一层一层地套上不同的衣服(装饰器),每一件衣服都增加一种新的特性(功能),但物体的本质(核心功能)并没有改变。这个过程是通过创建一个包裹原始对象的“装饰器”对象来实现的,而不是通过继承。 装饰器模式是开闭原则的典范应用,它让我们在不修改既有代码的情况下,为对象赋予了无限的扩展可能。 UML 类图: 装饰器模式通常包含四个核心角色: Component (抽象构件): 这是一个接口或抽象类,它定义了原始对象和装饰器对象所共有的接口,确保它们可以被一致地对待。 ConcreteComponent (具体构件): 这就是我们的“裸对象”,即被装饰的原始对象,它实现了 接口。 Decorator (抽象装饰器): 它也实现了 接口,并且内部持有一个 类型的引用(即它要装饰的对象)。它的存在是为了将装饰逻辑与基础组件分离开。 ConcreteDecorator (具体装饰器): 这是实际的“衣服”。它继承自 ,负责向被装饰的对象添加具体的新功能。在实现 接口的方法时,它除了调用原始对象(通过持有的引用)的相应方法外,还会加上自己的“装饰”逻辑。 讲讲装饰器模式的应用场景 场景一:运行时动态扩展功能。 当我们希望在运行时根据不同条件为对象增添不同功能时,装饰器模式是绝佳选择。最经典的例子就是Java的I/O流。 是一个具体构件,我们可以用 来装饰它,为其增加缓冲功能以提高性能;还可以再用 来装饰,进一步为其增加读取基本数据类型的能力。这些装饰器可以任意组合,非常灵活。可以顺带阅读一下笔者写的这篇 Java IO 设计模式总结。 场景二:当继承方案不可行或不适用时。 如果一个类被 修饰,我们无法通过继承来扩展它。此时,装饰器模式就成了“救星”。此外,如果功能的组合种类非常多,使用继承会产生大量的子类,导致“类爆炸”问题,而装饰器模式则可以用少量的类灵活组合出所有需要的功能。 责任链模式 什么是责任链模式? 责任链模式(Chain of Responsibility Pattern)是一种 行为型设计模式 。它将请求的发送者和接收者解耦,通过创建一个处理请求的接收者链来处理请求。链上的每个接收者(也称为处理器或节点)都负责对请求进行一部分的处理或校验,并决定是否将请求传递给链上的下一个接收者,或者中断处理流程。 举个例子,你提交了一个电商订单,这个订单需要经过多个步骤的校验才能完成:库存校验 风控校验 支付信息校验 ... 订单完成 。每个校验步骤都像链条上的一个环节,只有通过当前环节的校验,订单才能进入下一个环节。任何一个环节校验失败,整个订单流程都会终止。 责任链模式的结构相对简单: Handler (处理器): 这是一个接口或抽象类,定义了处理请求的接口(如 ),以及一个指向下一个处理器的引用( / )。 ConcreteHandler (具体处理器): 它实现了 接口。在处理方法中,它首先判断自己是否能处理当前请求。如果能,就处理它;如果不能,就将请求传递给链上的下一个处理器。 讲讲责任链模式的应用场景 适用于多节点的流程处理,每个节点完成各自负责的部分,节点之间不知道彼此的存在,比如: 订单校验: 一个订单可能需要进行多种校验,例如商品库存校验、风控校验、支付信息校验等。可以将这些校验规则组成一个责任链,每个校验规则负责一种校验,如果校验不通过,则中断流程并返回错误信息;如果校验通过,则将请求传递给下一个校验规则。 OA的审批流: 不同的审批级别(例如部门经理、总经理等)组成一个责任链。每个审批级别都负责审批一部分内容,如果审批不通过,则中断流程并返回原因;如果审批通过,则将请求传递给下一个审批级别。 Filter(过滤器): 多