手写单例、工厂与代理

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

1. 五种单例实现逐一写出并分析并发与序列化安全性(含枚举单例)?

请写出五种单例实现,并逐一分析其并发与序列化安全性,其中包含枚举单例?

  • 饿汉、懒汉、双重检查、静态内部类、枚举单例
  • 并发安全性
  • 序列化与反射破坏

五种单例实现:1)饿汉式:类加载时即创建实例,绝对线程安全,但可能造成不必要的实例化;2)懒汉式(synchronized 方法):线程安全但每次访问都加锁,性能差;3)双重检查锁(DCL):先判空再加锁再判空,volatile 保证实例可见性,兼顾性能与安全;4)静态内部类:利用类加载机制,JVM 保证类加载时静态内部类只初始化一次,天然线程安全且懒加载;5)枚举单例:JVM 保证枚举实例只创建一次,天然防序列化与反射破坏,最安全。饿汉与静态内部类靠 JVM 类加载保证安全;DCL 需 volatile;懒汉同步性能差;枚举兼顾序列化安全(JVM 特殊处理枚举序列化)与反射安全。

并发安全的核心是"实例只创建一次且可见"。枚举单例之所以最安全,是因为 JVM 对枚举有特殊保护:反射不能实例化枚举、序列化返回同一实例。DCL 必须用 volatile 防止指令重排导致拿到未初始化完成的实例。

// 饿汉
public class Singleton1 { private static final Singleton1 INSTANCE = new Singleton1(); private Singleton1(){} public static Singleton1 get(){return INSTANCE;} }
// 懒汉同步
public class Singleton2 { private static Singleton2 s; public static synchronized Singleton2 get(){ if(s==null) s=new Singleton2(); return s; } }
// 双重检查 DCL
public class Singleton3 { private static volatile Singleton3 s; public static Singleton3 get(){ if(s==null){ synchronized(Singleton3.class){ if(s==null) s=new Singleton3(); } } return s; } }
// 静态内部类
public class Singleton4 { private static class Holder{ static final Singleton4 I=new Singleton4(); } public static Singleton4 get(){ return Holder.I; } }
// 枚举单例
public enum Singleton5 { INSTANCE; public void doSomething(){} }
#
★★★

2. 手写 Builder 模式,与构造器/静态工厂相比在可选参数与不可变对象上的优势如何?

请手写 Builder 模式,说明与构造器/静态工厂相比在可选参数与不可变对象上的优势?

  • Builder 的链式调用
  • 可选参数处理
  • 不可变对象构建

Builder 模式下,通过一个 Builder 类逐步设置字段,最后 build() 生成目标对象。相比构造器/静态工厂的优势:1)可选参数友好——字段多时不用写大量重载构造器(telescoping constructor),只设置需要的参数;2)可读性——链式调用语义清晰;3)不可变对象——目标类字段 final,Builder 设置后一次构建,对象不可变,适合线程安全;4)避免构造器参数顺序错误。典型实现:Builder 持有目标类的所有字段,build() 校验并 new 目标对象,目标类构造器为 private。

构造器参数多时重载爆炸且易错;Builder 把"参数设置"与"对象创建"分离,并能构造不可变对象。java.util.stream 的 collect 与 Lombok @Builder 都基于此思想。

public class User {
    private final String name; // final 不可变
    private final int age;
    private User(String name, int age){ this.name=name; this.age=age; }
    public static Builder builder(){ return new Builder(); }
    public static class Builder {
        private String name; private int age;
        public Builder name(String n){ name=n; return this; }
        public Builder age(int a){ age=a; return this; }
        public User build(){ return new User(name, age); }
    }
}
// User.builder().name("张三").age(20).build();
#
★★★

3. 手写 Spring 风格 IOC,注解扫描、依赖注入与循环依赖处理如何?

请手写一个 Spring 风格的 IOC 容器,说明注解扫描、依赖注入与循环依赖处理?

  • 注解扫描与 Bean 注册
  • 依赖注入(构造器/字段)
  • 三级缓存解决循环依赖

Spring 风格 IOC 容器:1)扫描:扫描指定包路径下的类,找出标注 @Component/@Service 等的类;2)注册:把类的 Class 放入容器,建立 beanName→BeanDefinition 映射;3)依赖注入:实例化时解析构造器/字段上的 @Autowired,从容器中获取依赖并注入;4)循环依赖:A 依赖 B,B 依赖 A。用三级缓存解决——singletonObjects(完全实例化)、earlySingletonObjects(已创建未完全注入)、singletonFactories(对象工厂)。A 创建时先放入三级缓存,注入 B 时发现 B 依赖 A,从三级缓存拿到 A 的早期引用(半成品)注入 B,B 创建完成后再回头把 A 完善。只有单例作用域、且依赖通过 setter/字段注入(非构造器)才能解决循环依赖。

二级缓存已经能解决"拿到半成品引用",但三级缓存加入 ObjectFactory,是为了支持在 A 创建过程中应用 AOP 代理等后处理,使早期引用也经过代理。核心是"先暴露半成品引用,再补全"。

public class SimpleIoc {
    Map<String, Class<?>> beanDefs = new HashMap<>();
    Map<String, Object> singletons = new HashMap<>(); // 一级缓存
    Map<String, Object> early = new HashMap<>();      // 二级缓存
    Map<String, Supplier<Object>> factories = new HashMap<>(); // 三级缓存
    public void register(String name, Class<?> clazz){ beanDefs.put(name, clazz); }
    public Object getBean(String name) {
        if (singletons.containsKey(name)) return singletons.get(name);
        if (early.containsKey(name)) return early.get(name);
        if (factories.containsKey(name)) { // 从三级缓存拿到早期引用
            Object o = factories.get(name).get();
            early.put(name, o);
            return o;
        }
        // 创建并注入依赖
        Object bean = createBean(name);
        singletons.put(name, bean);
        return bean;
    }
    Object createBean(String name){ /* 反射实例化 + 注入依赖 */ return null; }
}
#
★★★

4. Spring 的单例 Bean 与 GoF 单例的本质区别,容器作用域、实例生命周期与多例(prototype)替代

请说明 Spring 的单例 Bean 与 GoF 单例的本质区别,包括容器作用域、实例生命周期与多例(prototype)替代?

  • Spring 单例与 GoF 单例差异
  • 作用域与生命周期
  • prototype 替代

GoF 单例是"类级别保证整个 JVM 只有一个实例",由类自身通过私有构造器控制;Spring 单例是"容器作用域内一个 beanName 对应一个实例",由 Spring 容器管理,同一容器内默认每个 bean 只创建一个实例,但不同容器可以各自创建。本质区别:GoF 单例是类控制,Spring 单例是容器控制;Spring 单例可通过 scope 配置改为 prototype(每次获取新实例),而 GoF 单例无法做到。Spring 单例还受容器生命周期管理(init/destroy 回调、AOP 代理),与类的静态性无关。当需要"每次取新实例"时用 prototype 替代单例。

Spring 单例是"容器级单例",不是"类级单例"。理解这一点就明白为何 Spring 单例可配置、可被代理、可控制生命周期。prototype 是同一容器下让每次获取都新建实例的替代方案。

@Scope("prototype") // 每次获取都新建实例
@Component
public class PrototypeBean {}
// 默认 singleton @Scope("singleton")
#
★★

5. 手写 JDK 动态代理与 CGLIB 代理,说明字节码生成的差异与限制?

请手写 JDK 动态代理与 CGLIB 代理,说明字节码生成的差异与限制?

  • JDK 代理基于接口
  • CGLIB 基于继承
  • 两者的限制

JDK 动态代理:基于接口,代理类实现被代理对象的接口,通过 InvocationHandler 在运行期拦截方法调用,要求目标对象必须实现接口。CGLIB:基于继承,代理类继承目标类,通过生成子类字节码并重写方法实现代理,不要求接口,但对 final 类和方法无法代理(final 无法继承/重写)。差异:JDK 代理生成接口实现的代理类,CGLIB 生成目标类的子类;JDK 代理不需额外依赖,CGLIB 依赖字节码库。限制:JDK 代理只能代理接口,CGLIB 不能代理 final 类/方法。Spring 默认:目标实现接口用 JDK 代理,否则用 CGLIB。

找代理的本质是"在方法调用前后插入逻辑"。JDK 用接口实现,CGLIB 用子类继承。因此 JDK 代理依赖接口,CGLIB 受 final 限制。Spring 通过 proxyTargetClass 配置决定用哪种。

// JDK 动态代理
public interface Greet { void hi(); }
public class GreetImpl implements Greet { public void hi(){ System.out.println("hi"); } }
Greet proxy = (Greet) Proxy.newProxyInstance(
    GreetImpl.class.getClassLoader(),
    new Class[]{Greet.class},
    (p, method, args) -> { System.out.println("before"); Object r = method.invoke(new GreetImpl(), args); System.out.println("after"); return r; });
// CGLIB
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(GreetImpl.class);
enhancer.setCallback((MethodInterceptor)(obj, method, args, proxy) -> { System.out.println("before"); return proxy.invokeSuper(obj, args); });
Greet proxy2 = (Greet) enhancer.create();
#
★★

6. 手写一个简单 Spring 风格的依赖注入容器(注解扫描+构造注入)?

请手写一个简单的 Spring 风格依赖注入容器,实现注解扫描与构造注入?

  • 注解扫描
  • 构造注入
  • 容器管理与 Bean 解析

容器实现:1)扫描指定包下的类,筛选带 @Component 注解的类;2)对每个类,解析其构造器上 @Autowired 的参数类型,从容器获取对应 Bean;3)通过反射 newInstance 创建对象并注入依赖,存入容器;4)循环依赖通过三级缓存或提前暴露处理。构造注入是 Spring 推荐的注入方式,利于不可变与可测试性。容器需用 Map 保存 beanName→对象,并处理别名、类型查找。

依赖注入的本质是"容器负责创建对象并填充依赖",调用方不自己 new。构造注入把依赖作为构造器参数,保证对象创建即完整。容器用反射解析注解并组装依赖图。

@Component
public class ServiceA {}
@Component
public class ServiceB {
    private final ServiceA a;
    @Autowired
    public ServiceB(ServiceA a){ this.a = a; } // 构造注入
}
// 容器扫描:遍历包下类,对带 @Component 的类,用构造器参数解析依赖并实例化
#
★★

7. 手写装饰器与责任链,与代理模式的本质区别,以及何时用装饰器而非继承?

请手写装饰器与责任链模式,说明与代理模式的本质区别,以及何时用装饰器而非继承?

  • 装饰器与责任链结构
  • 与代理的本质区别
  • 装饰器 vs 继承

装饰器模式在不改变接口的前提下动态为对象添加职责,通过包装对象递归调用;责任链模式把多个处理器串成链,每个处理器决定是否处理或传给下一个。装饰器与代理的本质区别:装饰器是"增强原有对象的功能",对调用方透明,可组合多种装饰;代理是"控制对目标对象的访问",关注访问控制(如懒加载、权限、日志),通常只包一层。何时用装饰器而非继承:当需要动态组合多种职责、且避免继承爆炸(多个维度组合导致子类数量爆炸)时,用装饰器组合优于继承。装饰器与责任链都基于"包装/链式",但装饰器每个节点都处理并转发,责任链按条件决定谁处理。

装饰器强调"增强并组合",代理强调"控制访问"。用装饰器是因为组合优于继承:一个类有多个可选增强维度,继承会产生组合爆炸,装饰器可任意组合包装。

public interface Stream { void write(String s); }
public class FileStream implements Stream { public void write(String s){ /* 写文件 */ } }
public class BufferedStream implements Stream {     // 装饰器
    private final Stream inner;
    public BufferedStream(Stream s){ inner=s; }
    public void write(String s){ System.out.println("加缓冲"); inner.write(s); }
}
public class EncryptedStream implements Stream {    // 装饰器
    private final Stream inner;
    public EncryptedStream(Stream s){ inner=s; }
    public void write(String s){ System.out.println("加密"); inner.write(s); }
}
// 组合:new EncryptedStream(new BufferedStream(new FileStream()))
#
★★

8. 手写模板方法 + 工厂方法的框架扩展点组合,与策略模式的区别如何?

请手写模板方法 + 工厂方法的框架扩展点组合,说明与策略模式的区别?

  • 模板方法定义骨架
  • 工厂方法提供扩展点
  • 与策略模式的区别

模板方法模式在父类中定义算法骨架(固定步骤),把可变步骤留成抽象方法,由子类实现;工厂方法模式用工厂方法创建对象,让子类决定创建哪种对象。两者组合:模板方法中定义整体流程,其中"创建对象"这一步用工厂方法,由子类提供具体对象,从而在不改骨架的前提下扩展流程。与策略模式的区别:模板方法用"继承"实现,通过子类覆盖固定扩展点;策略模式用"组合",把算法封装成独立策略对象,在运行时替换。模板方法是"继承+固定骨架",策略是"组合+动态替换"。当算法骨架固定、只有个别步骤变化时用模板方法;当算法整体可替换时用策略。

模板方法+"钩子/工厂方法"让框架确定结构、子类填细节,属"继承复用";策略模式用"组合委派"让算法可替换。这是"继承 vs 组合"在模式上的体现。

public abstract class AbstractPipeline {
    public final void process() {          // 模板方法:固定骨架
        Object obj = createObject();        // 工厂方法扩展点
        stepBefore(obj);
        stepMain(obj);
        stepAfter(obj);
    }
    protected abstract Object createObject(); // 子类实现
    void stepBefore(Object o){}
    void stepMain(Object o){}
    void stepAfter(Object o){}
}
// 策略模式:算法对象可替换
public class Context { private Strategy s; public void setStrategy(Strategy s){ this.s=s; } public void run(){ s.execute(); } }
#
★★

9. 手写事件总线(发布订阅 + 异步线程池),如何避免监听器异常影响其他订阅者?

请手写一个事件总线(发布订阅 + 异步线程池),说明如何避免监听器异常影响其他订阅者?

  • 发布订阅机制
  • 异步线程池
  • 异常隔离

事件总线维护一个事件类型→监听器列表的映射。发布事件时遍历该事件的监听器并调用。可用同步或异步(线程池)执行。异步场景下,每个监听器在独立任务中执行,即使某个监听器抛异常,也只影响该任务,不影响其他监听器;但需捕获每个监听器的异常:逐个 try-catch 包裹监听器调用,避免一个监听器异常中断后续监听器。同时记录异常日志、可提供失败回调。同步场景也要逐个 try-catch,防止一个监听器异常导致发布者或后续监听器终止。

事件总线的关键:监听器之间要"隔离"——一个监听器失败不能影响其他监听器与发布者。逐个 try-catch + 异步线程池(每监听器独立任务)是常用做法。还要注意监听器解绑避免内存泄漏。

public class EventBus {
    private final Map<Class<?>, List<Consumer<Object>>> listeners = new ConcurrentHashMap<>();
    private final ExecutorService pool = Executors.newCachedThreadPool();
    public void subscribe(Class<?> type, Consumer<Object> c){ listeners.computeIfAbsent(type, k->new CopyOnWriteArrayList<>()).add(c); }
    public void publish(Object event) {
        List<Consumer<Object>> l = listeners.get(event.getClass());
        if (l == null) return;
        for (Consumer<Object> c : l) {
            pool.submit(() -> {
                try { c.accept(event); } catch (Exception e) { /* 隔离异常,不影响其他监听器 */ }
            });
        }
    }
}
#
★★

10. 手写观察者模式,同步/异步通知、事件总线与内存泄漏(未解绑)如何取舍?

请手写观察者模式,说明同步/异步通知、事件总线与内存泄漏(未解绑)的取舍?

  • 观察者注册与通知
  • 同步/异步通知
  • 未解绑导致的内存泄漏

观察者模式:主题维护观察者列表,状态变化时通知所有观察者。同步通知:观察者直接在当前线程执行,简单、实时,但主题被观察者阻塞;异步通知:用线程池执行观察者,不阻塞主题,但顺序无法保证、需处理并发。取舍:同步适合对顺序敏感、逻辑轻量的场景;异步适合耗时逻辑、需要解耦的场景。内存泄漏:若观察者被注册到主题但从未解绑,而主题是长生命周期对象(如全局事件总线),观察者会被强引用,无法被 GC,造成泄漏。解决:观察者用完调用 remove 解绑,或用弱引用持有观察者。

观察者与主题是"强引用注册"关系,未解绑 + 长生命周期主题 = 泄漏。事件总线本质是观察者模式的扩展。同步/异步的取舍在于"实时性 vs 解耦/不阻塞"。

public class Subject {
    private final List<Observer> observers = new ArrayList<>();
    private final ExecutorService pool = Executors.newCachedThreadPool();
    public void attach(Observer o){ observers.add(o); }
    public void detach(Observer o){ observers.remove(o); } // 解绑,防止泄漏
    public void notifyChange() {
        for (Observer o : observers) {
            pool.submit(() -> o.update()); // 异步通知
        }
    }
}
#
★★

11. 手写 JDK 动态代理 vs CGLIB,接口代理与类代理的限制对比如何?

请手写 JDK 动态代理与 CGLIB,对比接口代理与类代理的限制?

  • JDK 代理接口限制
  • CGLIB 类代理与 final 限制
  • 选择依据

JDK 动态代理只能代理接口——代理类通过 Proxy.newProxyInstance 实现目标接口,因此目标对象必须实现接口,否则无法代理。CGLIB 通过生成目标类的子类来代理,不要求接口,但目标类不能是 final,目标方法不能是 final(final 方法无法被子类重写)、静态方法也无法代理。对比:JDK 代理要求接口、使用 JDK 原生反射、无额外依赖;CGLIB 不要求接口、需要字节码库、能代理非 final 类。选型:目标有接口用 JDK 代理(Spring 默认),无接口用 CGLIB。最终实现都是通过 InvocationHandler/MethodInterceptor 拦截方法。

限制的本质是代理实现机制:JDK 靠接口实现,CGLIB 靠继承重写。因此"接口/非 final 类"决定可用性。Spring 自动按此切换。

// JDK:需要接口
Greet proxy = (Greet) Proxy.newProxyInstance(cl, new Class[]{Greet.class}, handler);
// CGLIB:不需要接口,superclass 不能是 final
Enhancer e = new Enhancer();
e.setSuperclass(ConcreteClass.class); // 若 ConcreteClass 是 final 则失败
e.setCallback((MethodInterceptor)(obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
#
★★

12. 手写原型模式,浅/深拷贝(clone/序列化/拷贝构造器)与注册表原型

请手写原型模式,说明浅/深拷贝(clone/序列化/拷贝构造器)与注册表原型?

  • clone 浅拷贝与深拷贝
  • 序列化与拷贝构造器
  • 注册表原型

原型模式通过复制已有实例来创建新对象,避免重新构建。浅拷贝:clone() 复制基本类型和引用,引用对象共享;深拷贝:复制引用对象本身,需重写 clone 递归复制、用序列化(ObjectOutputStream 写读)或拷贝构造器。三者区别:clone 需实现 Cloneable(浅)、序列化需实现 Serializable 且性能低、拷贝构造器最清晰可控(可定制深拷贝)。注册表原型:把常用原型注册到 Map,通过 key 克隆得到新实例,避免每次 new。浅拷贝适合引用不可变场景,深拷贝适合引用可变需要独立副本的场景。

原型模式的价值是"克隆成本低于重新构建"。深拷贝用序列化最通用但慢,拷贝构造器最快最可控。注册表原型让原型可复用、按 key 快速克隆。

public class Prototype implements Cloneable {
    private List<String> list = new ArrayList<>();
    @Override public Prototype clone() {       // 浅拷贝
        try { return (Prototype) super.clone(); } catch (CloneNotSupportedException e){ throw new RuntimeException(e); }
    }
    public Prototype deepClone() {              // 深拷贝(序列化)
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            new ObjectOutputStream(bos).writeObject(this);
            return (Prototype) new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())).readObject();
        } catch (Exception e){ throw new RuntimeException(e); }
    }
}
#
★★

13. MyBatis/Feign 如何用 JDK 动态代理把接口变成可调用实现,MapperProxy 与 InvocationHandler 的角色

请说明 MyBatis/Feign 如何用 JDK 动态代理把接口变成可调用实现,以及 MapperProxy 与 InvocationHandler 的角色?

  • JDK 动态代理生成接口实现
  • MapperProxy 作为 InvocationHandler
  • 方法调用映射到 SQL/HTTP

MyBatis 的 Mapper 是接口,没有实现类。MyBatis 用 JDK 动态代理为每个 Mapper 接口生成代理对象,代理的 InvocationHandler 是 MapperProxy。当调用 Mapper 接口方法时,MapperProxy.invoke() 被触发,解析方法对应的 MapperMethod(通过方法所在接口 + 方法签名定位到 XML/Mapper 注解中的 SQL),执行 SQL 并返回结果。Feign 同理:为 @FeignClient 接口生成代理,invoke 时根据方法注解构HTTP 请求并由 Ribbon 选实例后发送。本质:用动态代理把"接口方法调用"转成"底层操作(SQL/HTTP)",让调用方像调用普通方法一样。

动态代理的价值在于"接口没有实现类也能被调用"——代理在 invoke 里拦截方法并映射到具体动作。MyBatis 的 MapperProxy 和 Feign 的 FeignInvocationHandler 都是 InvocationHandler 的实现。

// MyBatis 简化
public class MapperProxy<T> implements InvocationHandler {
    private final Class<T> mapperInterface;
    private final SqlSession session;
    public Object invoke(Object proxy, Method method, Object[] args) {
        MapperMethod mm = resolve(method); // 解析 SQL
        return mm.execute(session, args);  // 执行 SQL 并返回
    }
}
// 生成代理
T mapper = (T) Proxy.newProxyInstance(cl, new Class[]{mapperInterface}, new MapperProxy<>(mapperInterface, session));
#
★★

14. FactoryBean 与 @Bean 工厂方法,Spring 中工厂模式的两种实现、生命周期差异与典型用途

请说明 FactoryBean 与 @Bean 工厂方法,即 Spring 中工厂模式的两种实现,分析其生命周期差异与典型用途?

  • FactoryBean 接口
  • @Bean 工厂方法
  • 生命周期与用途差异

FactoryBean 是一个接口,实现 getObject() 返回创建的对象,Spring 容器中注册的是"工厂",通过 &beanName 获取工厂本身,通过 beanName 获取 getObject() 的结果。@Bean 方法在 @Configuration 类中声明,方法返回的对象作为 Bean 注册。两者都是工厂模式在 Spring 中的体现。差异:生命周期上,FactoryBean 由容器实例化后调用 getObject 创建目标对象,可控制创建逻辑;@Bean 方法由容器调用方法得到对象。用途:FactoryBean 常用于复杂对象的创建(如 SqlSessionFactoryBean、ProxyFactoryBean 结合 AOP 生成代理),@Bean 用于模块化配置(如组装第三方依赖、数据库连接池)。@Bean 方法若被 CGLIB 代理拦截,可保证单例。

两者都是"工厂方法创建 Bean",但 FactoryBean 是接口约定、@Bean 是方法声明。FactoryBean 让 Bean 本身成为"工厂"(如生成代理),@Bean 更直观地在一个配置类里声明多个 Bean。

@Component
public class MyFactoryBean implements FactoryBean<MyObj> {
    public MyObj getObject() { return new MyObj(); } // 工厂方法创建对象
    public Class<?> getObjectType() { return MyObj.class; }
    public boolean isSingleton() { return true; }
}
// 获取:ctx.getBean("myFactoryBean") -> MyObj;ctx.getBean("&myFactoryBean") -> 工厂本身
// @Bean 工厂方法
@Configuration
public class Config {
    @Bean
    public DataSource dataSource() { return new HikariDataSource(); }
}
#

15. 手写策略+工厂,如何消除 if-else 分支并保持可扩展?

请手写策略 + 工厂,说明如何消除 if-else 分支并保持可扩展?

  • 策略模式封装算法
  • 工厂按类型创建策略
  • 消除 if-else 与可扩展

策略 + 工厂:把每种算法封装为独立策略类(实现统一接口),工厂根据传入的类型从 Map 中取出对应策略,避免 if-else 分支。调用方只依赖策略接口,不关心具体实现。扩展时新增策略类并在工厂注册即可,不改动现有代码(开闭原则)。实现:工厂用 Map<String, Strategy>(或按类型/枚举)注册策略,通过 get(type) 返回策略。可配合 Spring 把策略类注入容器,或用注解标注策略类型。

if-else 的问题是新分支要改原方法,违反开闭原则;策略+工厂把"选择逻辑"集中到工厂,把"算法"封装到策略类,新增策略只需注册,不改逻辑。这是"用多态+映射替代分支"。

public interface PayStrategy { void pay(double amount); }
public class WechatPay implements PayStrategy { public void pay(double a){ /* 微信 */ } }
public class AlipayPay implements PayStrategy { public void pay(double a){ /* 支付宝 */ } }
public class PayFactory {
    private static final Map<String, PayStrategy> map = new HashMap<>();
    static { map.put("wechat", new WechatPay()); map.put("alipay", new AlipayPay()); }
    public static PayStrategy get(String type){ return map.get(type); } // 消除 if-else
}
// 使用:PayFactory.get("wechat").pay(100);
#

16. 手写适配器/外观,对接第三方库时如何隔离 API 变化,与代理的边界如何?

请手写适配器/外观模式,说明对接第三方库时如何隔离 API 变化,以及与代理的边界?

  • 适配器转换接口
  • 外观统一入口
  • 与代理的边界

适配器模式把第三方库的接口转换为业务期望的接口,让业务代码只依赖自己定义的接口,第三方库变化时只需改适配器。外观模式为子系统提供一个统一的高层接口,屏蔽内部复杂性。对接第三方库时,用适配器/外观封装第三方 API,业务层不直接依赖第三方类型,从而隔离 API 变化(升级/换库只改适配层)。与代理的边界:代理控制访问(权限、懒加载、日志),不改变接口;适配器改变接口(转换),目的是兼容;外观是简化入口。适配器重"接口转换",代理重"访问控制",外观重"简化门面"。

适配器解决"接口不匹配",外观解决"内部复杂",代理解决"访问控制"。隔离第三方变化用适配器/外观,把易变点集中在边界层。

// 第三方库
public class ThirdPartySms { public void sendEms(String m){ /* 第三方 API */ } }
// 业务期望接口
public interface SmsSender { void send(String msg); }
// 适配器:转换接口,隔离第三方
public class SmsAdapter implements SmsSender {
    private final ThirdPartySms third = new ThirdPartySms();
    public void send(String msg){ third.sendEms(msg); } // 转换
}
// 业务只依赖 SmsSender,第三方变化只改适配器
#

17. 手写简单工厂、工厂方法与抽象工厂,三种工厂的演进关系与各自适用边界如何?

请手写简单工厂、工厂方法与抽象工厂,说明三种工厂的演进关系与各自适用边界?

  • 简单工厂
  • 工厂方法
  • 抽象工厂

简单工厂:一个静态方法根据参数返回不同产品,集中创建逻辑,但违反开闭原则(新增产品要改工厂)。工厂方法:工厂类定义抽象创建方法,子类决定创建哪种产品,客户端面向抽象工厂,符合开闭原则,但每种产品对应一个工厂类。抽象工厂:生产"一族相关产品",每个具体工厂返回一族产品对象,适合产品族维度(如不同操作系统皮肤),但要扩展产品族新增产品类型较难。演进关系:简单工厂最简,工厂方法解决"一种产品多实现",抽象工厂解决"多产品族"。适用边界:产品少用简单工厂,产品类型多且可扩展用工厂方法,产品分族并且族内需配套用抽象工厂。

三种是"创建对象"的抽象程度递进。简单工厂不抽象、工厂方法把创建延迟到子类、抽象工厂创建一族产品。选型看产品维度与扩展需求。

// 简单工厂
class SimpleFactory { public static Product create(String type){ return type.equals("a")?new A():new B(); } }
// 工厂方法
abstract class Factory { abstract Product create(); }
class AFactory extends Factory { Product create(){ return new A(); } }
// 抽象工厂
interface AbstractFactory { ProductA createA(); ProductB createB(); }
class Factory1 implements AbstractFactory { public ProductA createA(){ return new A1(); } public ProductB createB(){ return new B1(); } }
#

18. 手写 JDK 动态代理,InvocationHandler 与 Proxy.newProxyInstance 的原理如何?

请手写 JDK 动态代理,说明 InvocationHandler 与 Proxy.newProxyInstance 的原理?

  • InvocationHandler 接口
  • Proxy.newProxyInstance 参数
  • 代理生成原理

Proxy.newProxyInstance(ClassLoader, Class<?>[] interfaces, InvocationHandler) 在运行时生成一个代理类,该代理类实现传入的接口,并持有 InvocationHandler。调用代理对象的任何接口方法时,方法调用被转发到 InvocationHandler.invoke(proxy, method, args),在此自定义处理逻辑(如 before/after、调用真实对象)。原理:JDK 在运行期用字节码生成器动态创建代理类(实现接口),方法调用通过反射调用 handler.invoke。InvocationHandler 是拦截与转发的中枢。真实对象方法需在 invoke 里手动通过 method.invoke(target, args) 调用。

动态代理的核心是"运行期生成接口实现类 + 方法调用转发到 handler"。这正是 MyBatis/Spring AOP 的基础。参数中 ClassLoader 用于加载代理类,interfaces 是代理要实现的接口。

public class LogHandler implements InvocationHandler {
    private final Object target;
    public LogHandler(Object t){ target = t; }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before " + method.getName());
        Object r = method.invoke(target, args); // 调用真实对象
        System.out.println("after");
        return r;
    }
}
Greet proxy = (Greet) Proxy.newProxyInstance(
    Greet.class.getClassLoader(),
    new Class[]{Greet.class},
    new LogHandler(new GreetImpl()));
#

19. 手写对象工厂的懒加载与缓存(Supplier + 双重检查)

请手写对象工厂的懒加载与缓存,用 Supplier + 双重检查实现?

  • Supplier 提供创建逻辑
  • 懒加载与缓存
  • 双重检查锁

对象工厂用 Supplier 提供对象的创建逻辑,首次调用时才创建(懒加载)并用 volatile 字段缓存,避免重复创建。双重检查:先检查缓存字段是否为 null,非空直接返回;为 null 则加锁,锁内再检查一次(第二次判空),确认为 null 才调用 supplier.get() 创建并赋值。volatile 保证实例创建完成后的可见性(防止指令重排导致读到未初始化对象)。这样既懒加载又线程安全且性能好。

双重检查 + volatile 是"懒加载单例/工厂"的标准写法。第一层判空避免锁竞争,第二层判空保证单次创建,volatile 防止半初始化对象被读取。

public class LazyFactory<T> {
    private final Supplier<T> supplier;
    private volatile T instance; // volatile 保证可见性与有序性
    public LazyFactory(Supplier<T> s){ supplier = s; }
    public T get() {
        if (instance == null) {                 // 第一层判空
            synchronized (this) {
                if (instance == null) {         // 第二层判空(双重检查)
                    instance = supplier.get();
                }
            }
        }
        return instance;
    }
}
#

20. InvocationHandler 的异常传播,目标方法抛出受检异常时动态代理如何处理,以及 UndeclaredThrowableException 的成因

请说明 InvocationHandler 的异常传播:目标方法抛出受检异常时动态代理如何处理,以及 UndeclaredThrowableException 的成因?

  • 受检异常在代理中的传播
  • UndeclaredThrowableException 成因
  • 异常包装

invoke 方法声明抛出 Throwable,真实方法抛出的异常会原样传播给调用方。但若真实方法抛出的受检异常不在代理接口方法声明的 throws 列表中,代理类无法直接抛出该受检异常——因为代理方法没有声明它,编译器视角下违反受检异常规则。此时 JDK 会把该异常包装成 UndeclaredThrowableException(RuntimeException 子类)抛出。同理,如果 invoke 里抛出的异常不在接口方法声明内,也会被包装成 UndeclaredThrowableException。成因:代理方法按接口方法签名声明 throws,若抛出的异常未在其中声明,就得用 Runtime 包装。

异常传播规则:接口方法声明的受检异常可原样传播;未声明的受检异常被包装成 UndeclaredThrowableException。业务上应尽量让接口方法声明可能抛出的异常,或处理后再抛出。

public Object invoke(Object proxy, Method method,
    Object[] args) throws Throwable {
    try { return method.invoke(target, args); }
    catch (InvocationTargetException e) { throw e.getCause(); } // 还原真实异常
}
// 若真实异常未在接口方法 throws 声明,代理会包装成 UndeclaredThrowableException