创建型模式高频

共 18 题
📑 题目列表 18 题
#
★★★

1. 单例模式五种实现(饿汉/懒汉/双检锁/静态内部类/枚举)的线程安全性对比?反射与反序列化如何破坏单例、如何防御?

请对比单例模式五种实现(饿汉/懒汉/双检锁/静态内部类/枚举)的线程安全性,并说明反射与反序列化如何破坏单例、如何防御?

  • 五种实现的线程安全对比
  • 双检锁的 volatile 与可见性
  • 反射/反序列化破坏单例与防御
  • 饿汉(eager):类加载时初始化实例,线程安全(JVM 保证类加载唯一),但类加载即创建,可能浪费资源。

  • 懒汉(lazy):首次调用才创建,用 synchronized 方法保证线程安全,但每次调用加锁,性能差。

  • 双检锁(DCL):首次检查 + 加锁 + 二次检查,实例用 volatile 修饰,防止重排序导致拿到半初始化对象;线程安全且性能好,是推荐的懒加载实现。

  • 静态内部类(holder):静态内部类持有实例,只有首次访问内部类时才初始化,由 JVM 类加载保证线程安全,无锁,推荐。

  • 枚举(enum):JVM 天然保证枚举单例的线程安全、反序列化安全、反射防御,是最简单且最安全的实现。

  • 反射破坏:通过 Constructor.setAccessible(true) 调用私有构造器可创建新实例,破坏单例。防御:构造器内检查 instance 非空则抛异常,或使用枚举(JVM 禁止反射创建枚举实例)。

  • 反序列化破坏:反序列化会创建新实例(不调用构造器)。防御:实现 readResolve() 返回现有 instance,或使用枚举(JVM 对枚举反序列化保证单例)。

线程安全轻松实现靠枚举/静态内部类;反射通过构造器防,反序列化通过 readResolve/枚举防。枚举是最安全实现,但无法继承。

// 双检锁
class Singleton {
    private static volatile Singleton instance;
    private Singleton() { if (instance != null) throw new RuntimeException("单例被破坏"); }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) instance = new Singleton();
            }
        }
        return instance;
    }
    // 防御反序列化
    protected Object readResolve() { return instance; }
}
// 枚举
enum SingletonEnum { INSTANCE; }
#
★★★

2. 简单工厂、工厂方法、抽象工厂的本质区别与演进关系?各自违反/满足开闭原则的边界?

请解释简单工厂、工厂方法、抽象工厂的本质区别与演进关系,说明各自违反/满足开闭原则的边界?

  • 三种工厂的职责与结构
  • 开闭原则的满足情况
  • 演进关系
  • 简单工厂(Simple Factory):一个工厂类根据参数用 if/switch 创建不同产品。创建逻辑集中在一个类,新增产品要改工厂类的判断逻辑,违反开闭原则(对修改封闭)。
  • 工厂方法(Factory Method):把创建延迟到子类,父类定义抽象工厂方法,子类覆盖实现创建具体产品。新增产品只需新增一个子类工厂,满足开闭原则(对扩展开放、对修改关闭),但每种产品需一个工厂类。
  • 抽象工厂(Abstract Factory):创建"产品族"(一系列相关产品),工厂接口定义多个工厂方法,每个具体工厂创建一族产品。新增产品族时新增工厂类,满足开闭原则;但新增产品(同族再加一个产品)要改工厂接口,违反开闭原则

演进关系:简单工厂 → 工厂方法(把工厂类抽象,解决扩展)→ 抽象工厂(针对产品族)。简单工厂是"集中判断",工厂方法/抽象工厂是"多态 + 继承"。

开闭边界:简单工厂每次加产品判改 switch(违反);工厂方法加产品加子类(满足);抽象工厂加产品族加工厂(满足),但加同族产品要改接口(违反)。演进是"从集中到多态"。

#
★★★

3. 原型模式与深/浅拷贝中 Cloneable 的坑与序列化深拷贝的取舍?

请解释原型模式与深浅拷贝,说明 Cloneable 的坑、序列化深拷贝的取舍?

  • 原型模式(clone 创建新对象)
  • 浅拷贝 vs 深拷贝
  • Cloneable 的坑与序列化深拷贝
  • 原型模式:通过复制已有原型对象来创建新对象,而不是 new。适用于对象创建成本高、属性差异小的场景。Java 用 Cloneable 接口 + Object.clone()
  • 浅拷贝:只复制引用,不复制引用的对象,新旧对象共享内部对象;深拷贝:连内部对象一起复制,新旧完全独立。
  • Cloneable 的坑:clone() 是 protected,需实现 Cloneable 并覆盖;默认是浅拷贝(引用共享);数组/集合的 clone 只复制数组引用不是元素深拷贝;hashCode 不变等。若内部有可变对象,浅拷贝会导致共享修改。
  • 序列化深拷贝:把对象序列化再反序列化得到深拷贝,可复制复杂对象图。取舍:实现简单(对象需 Serializable),但性能差(序列化开销大)、有安全风险(反序列化)、且不适用于所有类型(如不可序列化、单例)。替代方案:手写深拷贝(复制构造器,copy constructor)、逐个复制可变字段。

原型模式要区分深浅拷贝。浅拷贝简单但共享可变对象有风险,深拷贝用序列化实现简单但性能差,复杂对象优先考虑复制构造器/手写深拷贝。

// 深拷贝(复制构造器)
class Order {
    final List<Item> items;
    Order(Order o) { this.items = new ArrayList<>(o.items); } // 深拷贝 items
}
#
★★★

4. 项目中你用过哪些创建型模式?结合框架举例,Spring 的 BeanFactory/ApplicationContext 工厂、默认单例 Bean、原型 Bean、Builder 构建配置对象

请结合项目与框架举例你使用过的创建型模式,如 Spring 的 BeanFactory/ApplicationContext 工厂、默认单例 Bean、原型 Bean、Builder 构建配置对象?

  • Spring 容器作为工厂(BeanFactory/ApplicationContext)
  • 单例 Bean 与原型 Bean
  • Builder 构建配置对象
  • 工厂模式:Spring 的 BeanFactory/ApplicationContext 是工厂(IoC 容器),根据 Bean 定义创建并管理对象,把"创建"与"使用"解耦。FactoryBean 自定义工厂创建复杂 Bean。
  • 单例模式:Spring 默认作用域是单例(每个 Bean 单例),容器保证同一 Bean 只有一个实例,线程安全需 Bean 无状态。原型 Bean(scope=prototype)每次获取新建实例,适合有状态 Bean。
  • 工厂方法:getBean() 按类型/名称获取实例,@Bean/@Configuration 定义创建方法。
  • Builder 模式:配置类(如 RestTemplateHttpClientMongoClient 的 Builder)用 Builder 构建复杂的不可变配置对象,避免长参数构造器。项目中常用 Builder 构建配置 DTO。
  • 抽象工厂:DataSourceConnectionFactory 等创建一组相关对象(连接等)。

框架大量使用创建型模式:Spring 容器是工厂,默认单例 Bean + 原型 Bean,配置对象用 Builder,@Bean 可视为工厂方法。理解这些模式在框架中的体现能展示工程实践。

#
★★

5. Prototype 模式深拷贝的工程难题中循环引用的检测与处理(visited map)、不可变对象的安全共享、序列化/反序列化实现 clone 的性能代价与替代方案(copy constructor)

请解释 Prototype 模式深拷贝的工程难题,说明循环引用的检测与处理(visited map)、不可变对象的安全共享、序列化反序列化实现 clone 的性能代价与替代方案(copy constructor)?

  • 循环引用的检测(visited map)
  • 不可变对象安全共享
  • 序列化深拷贝的性能代价与 copy constructor

深拷贝复杂对象图时面临几个工程难题:

  • 循环引用:对象 A 引用 B、B 又引用 A(或更长的环),若简单递归深拷贝会无限递归/栈溢出。处理:维护一个 visited map(原对象 → 新对象),拷贝时先查 map,已拷贝则直接返回对应新对象,避免重复拷贝与死循环,从而正确处理环。
  • 不可变对象的安全共享:不可变对象(String、Integer、不可变配置)内容不会变,深拷贝无需复制,可安全共享引用(新对象直接引用原不可变对象),节约内存。深拷贝时对不可变对象浅拷贝即可。
  • 序列化深拷贝的性能代价:序列化 + 反序列化实现深拷贝,实现简单(对象需 Serializable),但性能差(序列化/反序列化开销大、需处理对象图)、对非序列化类型不可用、有反序列化安全风险、且不保证不可变共享。
  • 替代方案:手写深拷贝(逐个复制可变字段)、copy constructor(复制构造器,new 对象并复制字段)更可控、性能好、类型安全;或用工具库(如 MapStruct 的深度映射)。复杂对象优先用 copy constructor / 显式深拷贝而非序列化。

深拷贝难题是"环、不可变共享、性能"。visited map 处理环,不可变对象共享优化,copy constructor 替代序列化以兼顾性能与安全。

#
★★

6. Builder vs Abstract Factory 的选型标准中产品族一致性用 Abstract Factory、复杂对象分步构建用 Builder、两者组合使用(Director 持有 Abstract Factory)的场景

请解释 Builder 与 Abstract Factory 的选型标准,说明产品族一致性用 Abstract Factory、复杂对象分步构建用 Builder,以及两者组合使用(Director 持有 Abstract Factory)的场景?

  • Abstract Factory 解决产品族一致性
  • Builder 解决复杂对象分步构建
  • 两者组合(Director + Abstract Factory)
  • Abstract Factory:用于创建"一组相关产品"(产品族),保证族内产品一致性(如同一套 UI 风格、同一系列数据库操作)。工厂接口定义多个工厂方法,每个具体工厂创建一族产品。选型:需要"保证多产品风格/类型一致"时用。
  • Builder:用于分步构建"复杂对象",把构建过程与表示分离,支持可选参数、分步设置、不可变对象。选型:单个对象参数多、构建复杂、需要分步/校验时用。
  • 区别:Abstract Factory 关心"一组产品的一致性",Builder 关心"单个复杂对象的构建过程"。
  • 组合使用:Director(导演)持有 Abstract Factory,用工厂创建需要的部件,再用 Builder 按步骤组装成复杂对象。例如:Director 用 Abstract Factory 选颜色/材质,Builder 构建车辆,实现"产品族 + 复杂构建"的组合。

选型核心:产品族(多产品关联)用 Abstract Factory,复杂对象(单对象分步)用 Builder。当需"按产品族创建部件再组装复杂对象"时,二者组合(Director 持工厂)。

#
★★

7. 对象池模式与单例、工厂的关系中池化能降低高频对象的分配开销,池的回收与泄漏检测如何设计?

请解释对象池模式与单例、工厂的关系,说明为什么池化能降低高频对象的分配开销,以及池的回收与泄漏检测如何设计?

  • 对象池与单例/工厂的关系
  • 池化降低分配与 GC 开销
  • 回收与泄漏检测
  • 关系:对象池与工厂相关(工厂创建/借出对象),与单例不同——单例是"一个实例",对象池是"多个实例复用"。对象池是工厂的扩展(池化工厂),借出时从池取,归还时放回池。
  • 为什么池化降低开销:高频创建/销毁对象(如数据库连接、线程、缓冲)有分配与 GC 开销。池化复用对象,减少分配次数与 GC 压力,提升性能。适合创建成本高、被频繁使用的对象。
  • 回收:对象归还池时需重置状态(reset),避免脏数据;控制池容量(最大/最小),超限则新建或拒绝;池空时等待或新建。
  • 泄漏检测:借出对象若未归还,池资源会耗尽。检测:借出记录(谁借的、何时)、定期检查超时未归还对象、池容量监控、弱引用/引用计数检测未归还,或用 ThreadLocal 记录借用关系。配合日志与告警发现泄漏。

对象池 = 工厂 + 复用 + 回收。池化降低高频创建/GC 开销,但需重置与容量控制,泄漏检测靠借出跟踪与超时监控。

#
★★

8. 建造者模式与重叠构造器、JavaBeans 模式的对比中什么参数规模下引入 Builder 才划算?

请对比建造者模式与重叠构造器(telescoping constructor)、JavaBeans 模式,说明什么参数规模下引入 Builder 才划算?

  • 重叠构造器的问题
  • JavaBeans 模式的问题
  • Builder 的适用参数规模
  • 重叠构造器(telescoping):提供多个不同参数数量的构造器(如构造器 1 个参数、2 个参数、3 个参数...)。参数多时构造器组合爆炸、可读性差、易传错参数顺序。
  • JavaBeans 模式:用无参构造器 + setter 逐个设置。参数多时对象不完整(中间状态)、可变对象不可做不可变、多线程下 setter 分步设置不安全。
  • Builder:用 Builder 分步设置参数,build() 生成不可变对象,可校验、可读性好、避免构造器爆炸与中间状态。
  • 什么参数规模划算:参数较少(1-3 个)时用普通构造器即可;参数多(通常 4 个以上)或可选参数多、参数有默认值时,引入 Builder 才划算。Builder 有一定代码量,参数少时收益低。
  • 结论:参数数量大、可选参数多、需要不可变对象时用 Builder;参数少用构造器/静态工厂。

Builder 解决"参数多 + 可选参数 + 不可变"的构建问题。参数规模是关键:参数少用构造器,参数多才用 Builder,避免过度设计。

#
★★

9. 依赖注入(DI)对工厂模式的替代中 Spring/Guice 的 IoC 容器如何消除手动工厂代码、何时仍需显式工厂(运行时动态类型、跨进程创建)、DI 框架的隐式依赖与测试困难

请解释依赖注入(DI)对工厂模式的替代,说明 Spring/Guice 的 IoC 容器如何消除手动工厂代码、何时仍需显式工厂,以及 DI 框架的隐式依赖与测试困难?

  • DI 容器替代手动工厂
  • 何时仍需显式工厂
  • DI 的隐式依赖与测试困难
  • DI 与工厂:DI 容器(Spring/Guice)本质是"高级工厂",根据配置/注解自动创建并注入依赖,消除手动工厂代码。开发者声明依赖,容器负责生产与生命周期管理。
  • 消除手动工厂:容器通过配置(@Bean、@Component、Guice Module)管理对象创建与依赖关系,开发者无需写 if/switch 工厂,只需声明依赖关系。
  • 何时仍需显式工厂:DI 容器适合"由容器管理的常规依赖",但以下情况仍需显式工厂:运行时动态类型(类型由运行数据决定,如根据配置选算法)、跨进程/动态创建(无法预知的创建)、需要复杂构造逻辑或非容器管理的对象、按需创建大量对象。
  • DI 的隐式依赖与测试困难:DI 依赖是"隐式"的(通过注解/注入,不在代码中显式可见),可读性差、配置错误难发现;大量依赖注入使对象初始化不确定,测试时需 mock 大量依赖,配置复杂。过度使用 DI 会让"依赖关系"隐藏在容器配置中,难以追踪。

DI 容器是好工厂,替代手动创建,但"运行时动态类型"仍需显式工厂;DI 的隐式依赖(依赖藏在配置里)与测试 mock 成本是它的代价。

#
★★

10. Java 枚举单例的优势与边界中天然防反射与序列化攻击、不能继承的局限、带状态的枚举单例(如线程安全的计数器)实现、与 Kotlin object 声明的对比

请解释 Java 枚举单例的优势与边界,说明其天然防反射与序列化攻击、不能继承的局限、带状态的枚举单例实现,以及与 Kotlin object 声明的对比?

  • 枚举单例的防反射/序列化
  • 不能继承的局限
  • 带状态枚举单例与 Kotlin object
  • 优势:Java 枚举单例天然线程安全(JVM 保证加载一次)、天然防反射(反射无法创建枚举实例,setAccessible 会被拒绝)、天然防序列化破坏(JVM 对枚举反序列化保证单例)。代码最简单。
  • 边界/局限:枚举类不能继承其他类(局限),不能懒加载(类加载即实例化),且语义上"枚举"表达有限项,不适合需要继承的复杂单例。
  • 带状态的枚举单例:枚举可以有字段与方法,实现有状态的单例(如线程安全的计数器)。可加 synchronized 方法保证原子性。
  • 与 Kotlin object 对比:Kotlin object 声明创建单例对象,同样线程安全、简洁,但 Kotlin object 可继承/实现接口(比枚举灵活),且反序列化需注意(Kotlin object 的反序列化与 Java 枚举不同,需额外处理)。Kotlin object 是语言级单例,Java 枚举是"利用枚举机制"。

枚举单例最安全(防反射/序列化/线程安全),但不能继承、无懒加载;Kotlin object 是语言级单例,更灵活但反序列化需关注。二者都简洁。

#
★★

11. 创建型模式的目标是解耦创建与使用中简单工厂仍违反开闭原则,工厂方法如何把变化点延迟到子类?

请解释创建型模式的目标是解耦创建与使用,说明为什么简单工厂仍违反开闭原则,以及工厂方法如何把变化点延迟到子类?

  • 创建与使用解耦
  • 简单工厂违反开闭
  • 工厂方法延迟到子类
  • 创建型模式的目标:把"对象的创建"与"对象的使用"解耦,让调用者不直接 new,而是通过工厂/Builder 等创建,从而分离变化点(创建逻辑集中、可扩展)。
  • 简单工厂违反开闭:简单工厂用一个类集中根据参数创建产品,创建逻辑(if/switch)写死在工厂类中。新增产品类型要修改工厂类的 switch 分支,对修改开放,违反开闭原则(对扩展开放、对修改关闭)。
  • 工厂方法延迟到子类:工厂方法在父类定义"创建产品"的抽象方法,具体创建逻辑由子类实现。新增产品时,新增一个子类工厂覆盖工厂方法即可,无需修改已有类,对扩展开放、对修改关闭。变化点(具体创建哪类产品)从父类延迟(下移)到子类,父类只依赖抽象的工厂方法,符合开闭原则。

创建与使用解耦是目标;简单工厂因"集中 switch 创建"违反开闭,工厂方法通过"子类覆写工厂方法"把创建变化点延迟到子类,实现对扩展开放。

#
★★

12. 单例模式在框架中的实现中 Spring 默认单例 Bean 的线程安全性,为什么“有状态单例”是隐患,如何用 ThreadLocal/无状态化解?

请解释单例模式在框架中的实现,说明 Spring 默认单例 Bean 的线程安全性,为什么"有状态单例"是隐患,以及如何用 ThreadLocal/无状态化解?

  • Spring 默认单例 Bean 的并发
  • 有状态单例的隐患
  • ThreadLocal/无状态化解
  • Spring 默认单例:Spring 默认 scope 是 singleton,容器为每个 Bean 创建一个共享实例,多线程并发访问同一单例 Bean。
  • 线程安全:单例 Bean 本身线程安全的前提是"无状态"(没有可变实例字段)。若 Bean 有可变共享状态(如计数器、缓存字段),多线程并发读写就会产生竞态。
  • 有状态单例的隐患:单例 Bean 被多线程共享,其可变字段成为共享变量,并发访问导致数据不一致、线程安全问题。例如在单例 Bean 中持有可变的请求上下文、计数器。
  • 化解:① 无状态设计——把可变状态外置(存数据库/缓存/参数传递),Bean 只做无状态逻辑;② 用 ThreadLocal 隔离——每个线程有自己的状态副本(如请求上下文放 ThreadLocal),避免共享;③ 用局部变量/原子类/锁保护必要共享状态。Spring 推荐无状态 Bean,有状态用原型 Bean 或 ThreadLocal。

单例 Bean 并发安全依赖"无状态"。有状态单例是隐患(共享可变状态),用无状态化 + ThreadLocal 隔离或原型 Bean 化解。

#
★★

13. 建造者模式与不可变对象的配合中为什么 Builder 常用于构建不可变 DTO/配置对象,与 Lombok @Builder 的实现差异?

请解释建造者模式与不可变对象的配合,说明为什么 Builder 常用于构建不可变 DTO/配置对象,以及与 Lombok @Builder 的实现差异?

  • Builder 构建不可变对象
  • 不可变 DTO/配置对象的价值
  • Lombok @Builder 的实现差异
  • Builder 与不可变对象的配合:不可变对象(字段 final、无 setter)无法在构造后修改,若参数多,用长构造器难读易错。Builder 用分步设置参数,最后 build() 一次性构造不可变对象,兼顾可读性与不可变性。
  • 为什么常用于 DTO/配置对象:DTO(数据传输)与配置对象通常是"一次性构造、之后不变",不可变安全(可安全共享、并发安全、无隐藏状态变更)。Builder 让这些对象的构造清晰、可校验、可读性好。
  • 与 Lombok @Builder 的实现差异:手写 Builder 需手动写 Builder 类、setter 式方法、build() 校验;Lombok @Builder 在编译期自动生成 Builder 类与静态方法,代码简洁。差异:Lombok 生成的 Builder 通常不校验(校验需配合 @Builder 自定义或手动),且对继承/泛型处理有局限;手写 Builder 更可控(可加校验、默认值逻辑)。Lombok 是"编译期生成",手写是"显式代码"。

Builder 让不可变对象(DTO/配置)构造清晰、可校验;Lombok @Builder 编译期生成 Builder 代码,简洁但校验/灵活性有限,手写更可控。

#
★★

14. 工厂的注册表实现中 JDBC DriverManager 与 Spring FactoryBean 如何用注册表按标识创建对象,与简单工厂的差异

请解释工厂的注册表实现,说明 JDBC DriverManager 与 Spring FactoryBean 如何用注册表按标识创建对象,以及与简单工厂的差异?

  • 注册表(registry)工厂
  • DriverManager 按驱动注册
  • FactoryBean 按名称/类型创建
  • 注册表工厂:工厂维护一个"注册表"(Map:标识 → 创建器/类),按传入的标识查找并创建对应对象。相比简单工厂的 if/switch,注册表用 Map 查找,扩展时注册新项即可,无需修改工厂。
  • JDBC DriverManager:通过 DriverManager.registerDriver(driver) 把驱动注册进列表,DriverManager.getConnection(url) 按 URL 前缀(jdbc:mysql/jdbc:postgres)匹配已注册的驱动并创建连接。扩展新数据库只需注册新驱动,符合开闭。
  • Spring FactoryBean:FactoryBean<T> 是创建复杂对象的工厂,getObject() 创建对象,getObjectType() 返回类型,isSingleton() 决定单例/原型。Spring 的 getBean(name) 按名称/类型从 Bean 注册表(BeanFactory 的 registry)查找并创建。BeanDefinition 注册表按名称管理 Bean 定义。
  • 与简单工厂差异:简单工厂用 if/switch 集中判断(新增要改 switch,违反开闭);注册表工厂用 Map 注册 + 查找(新增注册即可,符合开闭),更灵活、可动态扩展。

注册表工厂用"Map 注册 + 查找"替代 if/switch,扩展只需注册新项,符合开闭原则。DriverManager、Spring BeanFactory 都是注册表工厂。

#

15. 原型模式在什么场景比 Builder/工厂更合适(对象初始化成本高、属性差异小),原型池的并发安全如何保证?

请解释原型模式在什么场景比 Builder/工厂更合适,说明原型池的并发安全如何保证?

  • 原型模式的适用场景(初始化成本高、属性差异小)
  • 原型池(对象池)的并发安全
  • 与 Builder/工厂的对比
  • 适用场景:原型模式在"对象创建初始化成本高(如连数据库、复杂构造、加载配置)且需要创建多个属性差异小的对象"时比 Builder/工厂更合适。因为直接复制原型比重新初始化更高效,且只需复制后微调属性。工厂/Builder 每次都要走完整构造流程,原型 clone 复用初始化结果。
  • 原型池:把多个原型对象放入池中,需要时 clone 出副本。池化避免重复初始化,适合初始化成本高、频繁复制的场景。
  • 并发安全:原型池被多线程共享,需保证并发安全:池本身用线程安全结构(如 ConcurrentLinkedQueue、锁保护);clone 操作是只读的(原型自身不变),clone 出副本后各自独立,无共享状态;池的借出/归还用原子操作或锁。注意原型对象本身不可变,否则 clone 会让共享的原型状态被修改。

原型在"初始化成本高 + 属性差异小"时优于 Builder/工厂(复用初始化);原型池的并发安全靠"原型不可变 + 线程安全池 + clone 独立副本"。

#

16. 抽象工厂在跨平台 UI/主题系统中的实践中新增产品族时如何违反开闭原则,与工厂方法的组合使用?

请解释抽象工厂在跨平台 UI/主题系统中的实践,说明新增产品族时如何违反开闭原则,以及与工厂方法的组合使用?

  • 抽象工厂创建产品族(跨平台 UI)
  • 新增产品族 vs 新增产品的开闭边界
  • 与工厂方法组合
  • 跨平台 UI/主题系统:用抽象工厂创建一组相关产品(产品族)。例如"Windows 工厂"创建 Windows 按钮、Windows 对话框,"Mac 工厂"创建 Mac 按钮、Mac 对话框。客户端用抽象工厂接口创建整套 UI,保证产品族一致(同一平台风格)。
  • 违反开闭原则:新增产品族(如新增 Linux 平台)时,只需新增一个 Linux 工厂类,满足开闭原则;但新增产品(如新增一个"菜单"产品,所有平台都要加),需在抽象工厂接口加工厂方法,并让所有具体工厂实现,违反开闭原则(修改已有接口与所有实现)。
  • 与工厂方法组合:抽象工厂的每个工厂方法常用于工厂方法模式实现(每个具体工厂用工厂方法创建具体产品),抽象工厂负责产品族,工厂方法负责单个产品的创建延迟,两者组合提升灵活性。

抽象工厂对"新增产品族"开放(加工厂类),对"新增同族产品"关闭(要改接口与所有实现)。跨平台 UI 中,加平台满足开闭,加 UI 组件违反开闭。

#

17. ServiceLoader/SPI 中如何用 Java 的 ServiceLoader 实现可插拔工厂,与反射直接 newInstance 的取舍

请解释 Java 的 ServiceLoader/SPI,说明如何用 ServiceLoader 实现可插拔工厂,以及与反射直接 newInstance 的取舍?

  • SPI 与 ServiceLoader 机制
  • 可插拔工厂的注册加载
  • 与反射 newInstance 的取舍
  • SPI(Service Provider Interface):Java 内置的插件机制,通过 META-INF/services/<接口名> 文件列出实现类,ServiceLoader.load(接口.class) 加载所有实现。JDBC 驱动、日志框架(SLF4J)都基于 SPI。
  • 可插拔工厂:在接口文件里注册工厂实现类,ServiceLoader 按需加载,实现"可插拔"——新增实现只需在服务文件加一行,无需改代码。ServiceLoader 迭代返回实现实例,可挑选具体工厂。
  • 与反射直接 newInstance 的取舍:反射 newInstance 需要代码里知道类名(硬编码 Class.forName),加实现要改代码;ServiceLoader 从配置文件自动发现实现,无需改代码,更符合开闭、便于第三方扩展。但 ServiceLoader 有加载开销(扫描服务文件)、迭代可能加载多个实现、需处理顺序/选择;反射 newInstance 更直接但耦合类名。ServiceLoader 更适合"可插拔扩展",反射适合"已知类名的动态创建"。

ServiceLoader/SPI 用配置文件声明实现,实现"可插拔工厂",扩展无需改代码(符合开闭);反射 newInstance 需硬编码类名。ServiceLoader 更利于解耦扩展。

#

18. 对象作用域管理中 Spring 的 prototype/request/session scope 如何影响创建时机与线程安全,与默认单例的差异

请解释 Spring 对象作用域管理,说明 prototype/request/session scope 如何影响创建时机与线程安全,以及与默认单例的差异?

  • prototype/request/session scope 的创建时机
  • 各 scope 的线程安全
  • 与默认单例的差异
  • 默认作用域(singleton):容器创建一次,所有线程共享同一实例,创建时机是容器启动/首次获取时。线程安全依赖无状态。
  • prototype:每次 getBean() 或注入时创建新实例,创建时机是"每次获取",适合有状态或每次独立使用的对象。线程安全(每个实例独立),但每次新创建有开销。
  • request:每个 HTTP 请求一个实例,创建时机是请求生命周期内,请求结束销毁。适合请求级状态(如请求上下文),线程安全(每个请求独立)。
  • session:每个 HTTP 会话一个实例,创建时机是会话生命周期。适合会话级状态(如用户会话数据),线程安全(每个会话独立,但同一会话并发访问需注意)。
  • 与默认单例差异:单例"一个实例共享",prototype/request/session"按需/按作用域创建独立实例"。有状态对象用原型或作用域实例,无状态用单例。注意:单例 Bean 注入 prototype Bean 时,可通过 ObjectProvider/Proxy 拿到新实例。

scope 决定"创建时机与共享范围"。单例共享(无状态安全),prototype 每次新建(有状态安全),request/session 按请求/会话隔离状态。有状态用非单例避免共享污染。