003|接口与抽象类:设计取舍、默认方法以及面向接口编程

Java语言基础与面向对象约 12 分钟阅读6,940 字符
知识库文档 4 / 251

阅读时间:40-50 分钟 | 难度:中级 → 高级 | 前置知识:Java 基础语法、面向对象基本概念


一、从一个面试题开始

"接口和抽象类有什么区别?什么时候用接口,什么时候用抽象类?"

这是 Java 面试中出场率最高的问题之一,但 90% 的候选人的回答停留在"接口只能定义方法签名、抽象类可以有实现"这种教科书层面。真正区分高级工程师和普通工程师的,是对以下问题的理解深度:

  • 接口和抽象类在 JVM 字节码层面有什么本质区别?
  • Java 8 引入默认方法后,接口和抽象类的边界在哪里?
  • 为什么说"面向接口编程"是比"继承"更好的抽象手段?
  • 什么时候该用抽象类而不是接口?——大多数项目都用错了。

本文将逐一深入解答。


二、基础概念:接口是什么

2.1 语法定义

在 Java 中,接口(interface)是一种引用类型,它定义了一组方法签名(行为契约),但不提供实现(Java 7 之前)。任何类通过 implements 关键字声明自己"实现了"某个接口,就必须提供接口中所有方法的具体实现。

java
public interface PaymentService {
    PaymentResult pay(PaymentRequest request);
    PaymentStatus queryStatus(String transactionId);
}

2.2 接口的本质:能力契约

接口回答的是"这个对象能做什么"(capability / can-do),而不是"这个对象是什么"(identity / is-a)。这个区别是整个接口设计哲学的基石。

java
// 接口表达的是"能力"
public interface Flyable {
    void fly();
}

// 两个完全不相关的类可以共享同一个能力契约
public class Bird implements Flyable { ... }
public class Airplane implements Flyable { ... }

这个特性让接口成为 Java 实现多态的核心机制——不相干的类可以通过实现同一个接口,在同一个上下文中被统一对待。

2.3 接口的成员

成员类型 Java 7 Java 8+ Java 9+
常量(public static final) ✅ ✅ ✅
抽象方法 ✅ ✅ ✅
默认方法(default) ❌ ✅ ✅
静态方法(static) ❌ ✅ ✅
私有方法(private) ❌ ❌ ✅
私有静态方法 ❌ ❌ ✅

fig1 插入点:[接口与抽象类核心对比总览图] Fig1.mmd — 接口与抽象类在语法、语义、设计意图、JVM 层面的差异全景对比

253_fig1.png
253_fig1.png


三、基础概念:抽象类是什么

3.1 语法定义

抽象类(abstract class)是不能直接实例化的类,它可以包含抽象方法和具体方法。子类通过 extends 继承抽象类,并必须实现所有抽象方法(除非子类也是抽象类)。

java
public abstract class BaseRepository<T, ID> {
    // 具体方法:提供默认实现
    public Optional<T> findById(ID id) {
        return getEntityManager().find(getEntityClass(), id);
    }

    // 抽象方法:强制子类实现
    protected abstract EntityManager getEntityManager();
    protected abstract Class<T> getEntityClass();
}

3.2 抽象类的本质:模板与共享实现

抽象类回答的是"这些对象共享什么"(shared state and behavior)。它提供了一个半成品的骨架,让子类只需填充差异部分。

抽象类可以拥有:

  • 实例变量(状态)——这是接口没有的核心能力
  • 构造器——用于初始化共享状态
  • 非 public 方法——protected 方法可以暴露给子类但不暴露给外部
java
public abstract class AbstractCsvParser<T> {
    private final String filePath;          // 共享状态
    private final Charset charset;          // 共享状态

    protected AbstractCsvParser(String filePath, Charset charset) {
        this.filePath = filePath;
        this.charset = charset;
    }

    public final List<T> parse() {          // 模板方法

        List<String[ ]> rows = readCsv();

        return rows.stream()
            .map(this::mapRow)             // 抽象方法——子类填充
            .collect(Collectors.toList());
    }


    private List<String[ ]> readCsv() { ... } // 内部实现细节


    protected abstract T mapRow(String[ ] row); // 子类只需关心映射逻辑

}

四、语法层面的关键差异

4.1 对比总览

维度 接口 抽象类
关键字 interface abstract class
实现/继承 implements extends
多继承 支持多实现 单继承
实例变量 ❌ ✅
构造器 ❌ ✅
访问修饰符 方法默认 public 无限制
与 Object 的关系 无隐式继承 隐式继承 Object
可否实例化 ❌ ❌

4.2 多实现 vs 单继承

Java 不支持类的多继承(为了避免 C++ 的菱形继承问题),但允许一个类实现多个接口。这是接口最核心的设计优势之一:

java
public class OrderService implements
    Transactional,      // 事务能力
    Auditable,          // 审计能力
    Cacheable,          // 缓存能力
    MetricsCollectable  // 指标采集能力
{
    // 通过组合多个接口,灵活装配对象能力
}

4.3 访问控制的差异

接口中的方法默认是 public,且只能是 public。抽象类中的方法可以使用任意访问修饰符,这使得抽象类可以设计内部契约(protected 方法)而不暴露给外部调用者。

java
public abstract class ConnectionPool {
    // 对外暴露的接口
    public Connection getConnection() { ... }
    public void releaseConnection(Connection conn) { ... }

    // 子类可以重写的钩子方法,但不对外暴露
    protected Connection createConnection() { ... }
    protected boolean validateConnection(Connection conn) { ... }
}

五、设计层面的本质差异

5.1 is-a vs can-do

这是理解接口和抽象类最核心的思维模型:

  • 抽象类 → is-a 关系:子类是父类的一种。Dog is an Animal。
  • 接口 → can-do 关系:实现类具备某种能力。Dog can Run, can Swim, can be Trained。
java
// is-a:用抽象类建模
public abstract class Animal {
    protected String name;
    public abstract void breathe();
}

// can-do:用接口建模
public interface Runnable { void run(); }
public interface Swimmable { void swim(); }
public interface Trainable { void perform(String command); }

public class Dog extends Animal implements Runnable, Swimmable, Trainable {
    // Dog is an Animal (is-a)
    // Dog can Run, Swim, be Trained (can-do)
}

5.2 代码复用 vs 行为契约

  • 抽象类侧重代码复用:父类提供公共实现,子类继承复用。这是"自底向上"的思维。
  • 接口侧重行为契约:定义"必须做什么",不关心"怎么做"。这是"自顶向下"的思维。

一个判断标准:如果你发现自己在抽象类中写了大量 protected 方法和状态,它就是合理的抽象类使用场景;如果只是想定义"调用方可以依赖哪些方法",接口更合适。

5.3 版本演进的灵活性

接口在版本演进中有一个抽象类不具备的优势:向后兼容地添加默认方法。

java
// v1.0
public interface ConfigSource {
    String get(String key);
}

// v2.0:向后兼容地新增能力
public interface ConfigSource {
    String get(String key);

    // 新增:提供默认实现,不影响 v1.0 的实现类
    default String getOrDefault(String key, String defaultValue) {
        String value = get(key);
        return value != null ? value : defaultValue;
    }
}

fig2 插入点:[接口默认方法工作原理图] Fig2.png — 展示 default method 的优先级规则:类优先于接口、子接口优先于父接口

253_fig2.png
253_fig2.png


六、Java 8 之后:接口的进化

6.1 为什么引入默认方法?

Java 8 引入 Lambda 和 Stream API 时面临一个困境:如果在 Collection 接口中新增 stream() 方法,所有实现了 Collection 的第三方库(如 Guava、Apache Commons)都会编译失败。

默认方法解决了这个"接口演化问题"(interface evolution problem),让 JDK 可以在不破坏已有实现的情况下扩展核心接口。

java
public interface Collection<E> extends Iterable<E> {
    // Java 8 新增:Stream 支持
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

6.2 接口中的静态方法

Java 8 同时允许接口中定义静态方法,这改变了传统的"接口 + 伴生工具类"模式:

java
// 旧模式:接口 + 伴生类
public interface Comparator<T> {
    int compare(T o1, T o2);
}

public class Comparators {  // 伴生工具类
    public static <T> Comparator<T> naturalOrder() { ... }
}

// Java 8 新模式:静态方法直接放在接口中
public interface Comparator<T> {
    int compare(T o1, T o2);

    static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
        return (o1, o2) -> o1.compareTo(o2);
    }

    static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
        return (o1, o2) -> {
            if (o1 == null) return -1;
            if (o2 == null) return 1;
            return comparator.compare(o1, o2);
        };
    }
}

6.3 Java 9 私有方法

当一个接口有多个默认方法共享相同的逻辑时,这些逻辑可以抽取到私有方法中:

java
public interface EventProcessor {
    default void processEvent(Event event) {
        validateNotNull(event);
        doProcess(event);
        logProcess(event);
    }

    default void processBatch(List<Event> events) {
        events.forEach(this::validateNotNull);
        events.forEach(this::doProcess);
        events.forEach(this::logProcess);
    }

    // Java 9:私有方法,隐藏内部实现
    private void validateNotNull(Event event) {
        if (event == null) throw new IllegalArgumentException("Event cannot be null");
    }
    private void logProcess(Event event) {
        logger.debug("Processed event: {}", event.getId());
    }
}

fig3 插入点:[Java 接口演变时间线] Fig3.mmd — Java 7 → 8(default/static)→ 9(private)→ 17(sealed)→ 21 接口能力演进图

253_fig3.png
253_fig3.png


七、默认方法的优先级规则(类优先原则)

当一个类同时从父类和接口继承了相同签名的方法时,"类优先"(class wins)原则决定了最终调用哪个方法:

7.1 规则优先级

  1. 类优先于接口:父类中的具体方法优先于接口中的默认方法
  2. 子接口优先于父接口:更具体的接口默认方法优先
  3. 冲突时强制重写:两个无关接口提供相同签名默认方法时,实现类必须显式重写
java
interface A {
    default void hello() { System.out.println("A"); }
}

interface B extends A {
    default void hello() { System.out.println("B"); }  // 子接口优先
}

class Parent {
    public void hello() { System.out.println("Parent"); }  // 类优先
}

class Child extends Parent implements B {
    // 不需要重写 —— Parent.hello() 胜出(类优先原则)
}

// 冲突场景
interface X { default void greet() { System.out.println("X"); } }
interface Y { default void greet() { System.out.println("Y"); } }

class MyClass implements X, Y {
    // 必须显式解决冲突!
    @Override
    public void greet() {
        X.super.greet();  // 显式选择 X 的实现
    }
}

7.2 为什么设计成"类优先"?

这是向后兼容性的需要。如果接口默认方法可以覆盖类方法,那么当某个库的接口新增一个默认方法时,可能导致依赖该库的所有类的行为发生静默变化——这比编译错误更危险。

fig5 插入点:[多接口实现的菱形继承问题] Fig5.mmd — 菱形继承关系图、冲突检测流程、类优先原则和显式调用语法

253_fig5.png
253_fig5.png


八、抽象类 vs 接口:选型决策

8.1 核心判断标准

一个简单但有效的决策矩阵:

场景 选择 原因
需要共享状态/字段 抽象类 接口不能定义实例变量
需要构造器逻辑 抽象类 接口没有构造器
需要 protected/包级别方法 抽象类 接口方法必须 public
定义纯粹的 API 契约 接口 更轻量、更灵活
需要多继承能力 接口 Java 不支持多继承类
需要 Lambda/函数式接口 接口 抽象类不能用于 Lambda
期望后期可扩展方法 接口 可以用 default 方法向后兼容
模板方法模式 抽象类 父类定义骨架,子类填空

8.2 实战选型法则

优先选接口,除非满足以下条件之一:

  1. 你必须共享状态(实例变量)
  2. 你需要 protected 访问控制
  3. 你要实现模板方法模式(算法骨架)
  4. 你需要在构造器中初始化子类共用的逻辑
java
// ❌ 反例:应该用接口
public abstract class Logger {
    public abstract void log(String message);
    public abstract void error(String message);
}
// 这里不需要共享状态,应该定义为接口

// ✅ 正例:应该用抽象类
public abstract class BaseService {
    protected final DataSource dataSource;      // 需要共享状态
    protected final MetricsRegistry metrics;    // 需要共享状态

    protected BaseService(DataSource ds, MetricsRegistry mr) {
        this.dataSource = ds;
        this.metrics = mr;
    }

    // 模板方法:定义执行骨架
    public final Result execute(Request req) {
        metrics.increment("request.count");
        Timer.Context timer = metrics.startTimer("request.latency");
        try {
            validate(req);
            return doExecute(req);
        } finally {
            timer.stop();
        }
    }

    protected abstract void validate(Request req);
    protected abstract Result doExecute(Request req);
}

fig4 插入点:[抽象类 vs 接口选用决策流程图] Fig4.png — 基于 is-a/has-a/can-do 关系的完整选型决策树

253_fig4.png
253_fig4.png


九、JVM 层面的深入理解

9.1 字节码指令差异

JVM 对接口方法和类方法使用了不同的调用指令:

调用场景 字节码指令 分派方式
类实例方法 invokevirtual 动态分派,基于实际类型查虚方法表
接口方法 invokeinterface 动态分派,基于实际类型查找接口方法表
静态方法 invokestatic 静态分派,编译时确定
构造器/私有方法 invokespecial 静态分派,编译时确定
动态调用 invokedynamic 运行时引导方法决定

9.2 invokeinterface vs invokevirtual

关键区别在于方法分派的查找机制:

  • invokevirtual 使用虚方法表(vtable):每个类有一个固定偏移量的方法表,调用时直接通过偏移量查找,性能较高。
  • invokeinterface 使用接口方法表(itable):需要先找到类实现的接口列表,再在该接口的方法表中按方法签名搜索,性能略低于 invokevirtual。
java
// 示例:观察字节码差异
public class MethodDispatchDemo {
    public void testVirtual(ArrayList<String> list) {
        list.size();  // → invokevirtual (ArrayList 的方法)
    }

    public void testInterface(List<String> list) {
        list.size();  // → invokeinterface (List 接口的方法)
    }
}

// 用 javap -c 查看:
// invokevirtual #2  // Method java/util/ArrayList.size:()I
// invokeinterface #3, 1  // InterfaceMethod java/util/List.size:()I

fig7 插入点:[JVM invokeinterface 字节码指令调用流程] Fig7.mmd — 接口方法表的 itable 查找流程与虚方法表的 vtable 查找流程对比

253_fig7.png
253_fig7.png

9.3 默认方法的字节码实现

默认方法被编译为接口中的普通实例方法(非抽象),调用时仍使用 invokeinterface 指令。JVM 在类加载阶段解决默认方法的继承关系,生成符合"类优先"规则的最终方法映射表。


十、面向接口编程:从理念到实践

10.1 什么是面向接口编程

面向接口编程(Program to Interface, not Implementation)是 GoF 设计模式的核心原则之一:

"只要可能,你就应该引用接口类型而非具体类类型。"

java
// ❌ 紧耦合:依赖具体实现
public class OrderService {
    private MySQLOrderRepository repository = new MySQLOrderRepository();  // 直接依赖

    public Order findById(Long id) {
        return repository.findById(id);
    }
}

// ✅ 面向接口编程:依赖抽象
public class OrderService {
    private final OrderRepository repository;  // 依赖接口

    public OrderService(OrderRepository repository) {  // 依赖注入
        this.repository = repository;
    }

    public Order findById(Long id) {
        return repository.findById(id);
    }
}

// 现在可以轻松替换实现
new OrderService(new MySQLOrderRepository(dataSource));
new OrderService(new RedisCachedOrderRepository(redis, mysql));
new OrderService(new MockOrderRepository());  // 便于单元测试

10.2 三层价值

  1. 可测试性:可以用 Mock 实现替代真实实现进行单元测试
  2. 可替换性:切换底层实现不需要修改调用方代码
  3. 可扩展性:新增实现只需实现接口,无需修改现有代码(开闭原则)

10.3 接口隔离原则(ISP)

"客户端不应该被迫依赖它不使用的方法。"

253_fig6.png
253_fig6.png

java
// ❌ 违反 ISP:胖接口
public interface Worker {
    void work();
    void eat();
    void sleep();
}

// ✅ 遵循 ISP:接口拆分
public interface Workable { void work(); }
public interface Eatable { void eat(); }
public interface Sleepable { void sleep(); }

// 人类实现所有接口
public class Human implements Workable, Eatable, Sleepable { ... }

// 机器人不需要 eat 和 sleep
public class Robot implements Workable { ... }

fig6 插入点:[面向接口编程实战代码对比] Fig6.png — 紧耦合 vs 接口抽象 vs 依赖注入 三种写法的代码对比图


十一、接口的实战模式

11.1 标记接口(Marker Interface)

没有任何方法的接口,用于向 JVM 或框架传递元数据:

java
public interface Serializable { }  // 标记可序列化
public interface Cloneable { }     // 标记可克隆

// 自定义标记接口
public interface Auditable { }     // 标记需要审计
public interface CacheEvictable { } // 标记缓存可驱逐

注意:现在更推荐使用注解来替代标记接口(如 @Entity 替代了 JPA 中的标记接口模式),因为注解可以携带参数。

11.2 常量接口(反模式)

java
// ❌ 反模式:用接口定义常量
public interface Constants {
    int MAX_RETRY = 3;
    String DEFAULT_CHARSET = "UTF-8";
}

// ✅ 正确做法:使用 final 类或枚举
public final class AppConstants {
    private AppConstants() {}
    public static final int MAX_RETRY = 3;
    public static final String DEFAULT_CHARSET = "UTF-8";
}

常量接口的问题是它把实现细节暴露到了公共 API 中,任何实现该接口的类都会"污染"其命名空间。

11.3 函数式接口

只有一个抽象方法的接口,可以用 Lambda 表达式实现:

java
@FunctionalInterface
public interface EventHandler<T> {
    void handle(T event);

    // 可以包含多个 default/static 方法
    default EventHandler<T> andThen(EventHandler<T> after) {
        return event -> {
            handle(event);
            after.handle(event);
        };
    }
}

fig8 插入点:[接口在经典设计模式中的应用全景图] Fig8.png — 策略模式、适配器模式、代理模式、观察者模式中接口的核心角色图示

253_fig8.png
253_fig8.png


十二、常见陷阱与最佳实践

12.1 Serializable 与抽象类

java
public abstract class BaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    // 子类自动继承序列化能力
}

抽象类和接口都可以继承 Serializable,效果相同——但记得定义 serialVersionUID。

12.2 默认方法不能重写 Object 的方法

java
public interface Foo {
    // 编译错误!default 方法不能重写 Object 的方法
    // default boolean equals(Object o) { ... }
}

这是"类优先"原则的体现——Object 的方法永远优先于接口默认方法。

12.3 避免在接口中用 default 方法复写核心逻辑

java
// ❌ 反模式:default 方法中包含重要业务逻辑
public interface OrderValidator {
    boolean validate(Order order);

    // default 方法承担了过多逻辑
    default void validateAndThrow(Order order) {
        if (!validate(order)) {
            throw new OrderValidationException(...);
        }
        // 复杂的状态变更...
    }
}

default 方法的最佳用途是:提供便捷方法(convenience method)、可选扩展点、向后兼容的新功能。核心业务逻辑应该留在实现类中。

12.4 接口中 static 方法的继承

接口中的 static 方法不会被子接口或实现类继承——必须通过接口名直接调用:

java
interface Parser {
    static Parser create() { return new DefaultParser(); }
}

class MyParser implements Parser {
    void foo() {
        // MyParser.create();  // ❌ 编译错误!static 方法不被继承
        Parser.create();       // ✅ 必须通过接口名调用
    }
}

十三、现代 Java 接口新特性(Java 17/21)

13.1 Sealed 接口(Java 17)

密封接口允许你控制哪些类可以实现该接口:

java
public sealed interface PaymentMethod
    permits CreditCard, BankTransfer, DigitalWallet { }

public record CreditCard(String number, String cvv) implements PaymentMethod { }
public record BankTransfer(String iban) implements PaymentMethod { }
public record DigitalWallet(String walletId) implements PaymentMethod { }
// public class Cash implements PaymentMethod { }  // ❌ 编译错误!

结合 switch 的模式匹配,编译器可以进行穷尽性检查:

java
public String describe(PaymentMethod method) {
    return switch (method) {
        case CreditCard c -> "信用卡支付: " + c.number();
        case BankTransfer b -> "银行转账: " + b.iban();
        case DigitalWallet w -> "数字钱包: " + w.walletId();
        // 不需要 default 分支——编译器验证了穷尽性
    };
}

13.2 泛型接口的 PECS 原则

java
// Producer Extends, Consumer Super
public interface Repository<T> {
    void save(T entity);                          // Consumer → 可以用 ? super T
    Optional<T> findById(ID id);                  // Producer → 可以用 ? extends T

    // 灵活方法
    default void saveAll(Collection<? extends T> entities) {  // Producer
        entities.forEach(this::save);
    }
}

十四、核心要点总结

  1. 接口 = 能力契约(can-do),抽象类 = 模板骨架(is-a + 共享实现)
  2. Java 8 的默认方法让接口具备了有限的实现能力,但核心优势仍在于行为抽象而非代码复用
  3. 选型优先考虑接口,只有当需要共享状态、构造器、protected 访问、模板方法时才用抽象类
  4. "类优先"原则保证向后兼容性——类方法永远优先于接口默认方法
  5. JVM 层面 invokeinterface 使用 itable 查找,invokevirtual 使用 vtable 偏移,前者性能略低于后者
  6. 面向接口编程的核心价值:可测试、可替换、可扩展
  7. sealed 接口(Java 17)为接口增加了实现控制能力,配合模式匹配实现穷尽性检查

十五、延伸阅读

  • Effective Java (Joshua Bloch) — Item 20: Prefer interfaces to abstract classes
  • Effective Java (Joshua Bloch) — Item 21: Design interfaces for posterity
  • JLS §9: Interfaces
  • JLS §8.1.1.1: abstract Classes
  • JVM Spec §6.5: invokeinterface vs invokevirtual
  • Design Patterns: Elements of Reusable Object-Oriented Software (GoF) — Chapter 1: Program to an Interface, not an Implementation

十六、面试官视角:核心提问

以下问题从面试官视角设计,考察从基础到架构的多层次理解。建议在完成全文阅读后尝试回答。

基础层:验证语法与概念理解

Q1:一个类同时继承了抽象类和实现了接口,两者有相同签名的方法,最终调用哪个?为什么?

考察对"类优先"原则的理解以及设计动机的思考。

Q2:抽象类可以有构造器吗?接口可以有构造器吗?为什么?

考察对实例化语义和对象初始化流程的理解。

Q3:静态方法放在接口中 vs 放在工具类中,有什么实际区别?

考察对 API 设计、可发现性、命名空间管理的理解。

设计层:验证工程决策能力

Q4:你在设计一个"数据库连接池"时,会把对外接口设计为接口还是抽象类?关键决策点是什么?

考察在真实场景下做技术选型的思辨能力——连接池需要共享状态(连接池大小、活跃连接列表),同时需要定义对外契约(获取连接、释放连接)。

Q5:Spring Framework 中 **BeanPostProcessor**、**ApplicationListener** 都设计成了接口而不是抽象类,为什么?

考察对框架设计模式的洞察——Spring 需要用户实现多个扩展点,接口允许多实现/组合,抽象类的单继承会限制用户灵活使用。

Q6:如果一个接口已经被数百个类实现,你想要新增方法,有哪些策略?各有什么风险?

考察对接口演化策略的工程理解——default 方法、新接口继承、适配器模式等方案。

深层:验证原理与架构洞察

Q7:在 JVM 层面,为什么 **invokeinterface** 比 **invokevirtual** 有潜在的性能开销?现代 JVM 是如何优化的?

考察字节码级别的方法分派理解以及 JIT 编译器优化(内联缓存、CHA 等)。

Q8:Java 8 引入默认方法实质性削弱了接口和抽象类的差异——你认为当下两者真正的边界在哪里?

开放式问题,考察架构判断力——核心边界从"可否有实现"转移到"可否有状态"和"访问控制"。

Q9:如果让你设计一个分布式 RPC 框架的核心接口,你会如何平衡接口的稳定性和可演化性?

考察大规模系统的 API 设计能力——版本管理、兼容性策略、sealed 接口的应用等。

编码实战题

Q10:以下代码有什么问题?如何改进?

java
public interface DataProcessor<T> {
    default T process(T input) {
        T validated = validate(input);    // 调用自身的 validate
        T enriched = enrich(validated);    // 调用自身的 enrich
        return transform(enriched);        // 调用自身的 transform
    }
    default T validate(T input) { return input; }
    default T enrich(T input) { return input; }
    T transform(T input);
}

public class UserProcessor implements DataProcessor<User> {
    @Override
    public User transform(User input) { ... }
    // 使用了 process(),但没有重写 validate() 和 enrich()
    // 这合理吗?如果不合理,问题在哪?
}

考察对默认方法设计边界的理解。默认方法中调用其他可重写方法会创建脆弱的耦合——validate/enrich 的默认实现可能不是子类想要的。应该设计成抽象方法或使用模板方法模式来明确扩展点。


本文后续将补充配图。完成阅读后,请尝试独立回答上述 10 个面试题。

253_fig5.png
253_fig5.png

253_fig4.png
253_fig4.png

253_fig7.png
253_fig7.png

253_fig6.png
253_fig6.png

253_fig8.png
253_fig8.png