Skip to content

接口与抽象类 ​

学习目标 ​

  • 区分接口、抽象类和普通类的职责与扩展边界。
  • 理解 default、static、private 接口方法及多接口冲突解决。
  • 用依赖倒置和接口隔离设计可替换策略,并编写多态测试案例。

核心知识点 ​

专业术语 ​

  • 接口(interface):面向调用者的能力契约;一个类可以实现多个接口。
  • 抽象类(abstract class):不能直接实例化的基类,可共享实例状态、构造流程和部分实现。
  • default 方法:接口提供的默认实例实现,用于演进契约,但不能保存每个对象的业务状态。
  • 接口隔离原则(ISP):调用者不应被迫依赖它不使用的方法,应把大契约拆成小能力。
  • 依赖倒置原则(DIP):高层策略依赖抽象,具体实现通过构造器或工厂注入。

白话解释与边界 ​

接口回答“对象能做什么”,抽象类回答“同一族对象有哪些共用骨架”。接口字段隐含为 public static final,抽象类可以有构造器和受保护状态,但 Java 类只能直接继承一个父类。default 适合向后兼容的小行为,若两个接口提供同签名默认方法,实现类必须显式重写解决冲突;复杂领域状态不应藏进默认方法。

策略类只依赖接口,就能把支付、通知或格式化实现替换成真实服务、测试替身或新版本。接口越小,替代实现越容易;抽象类更适合需要共享校验和生命周期的紧密类型族。不要为了“可扩展”把所有方法都塞进一个接口。

常用用法 ​

interface/implements:声明并实现能力契约 ​

接口描述调用者需要的能力,一个类可以实现多个互不冲突的接口;实现方法必须满足接口可见性要求。

java
// 说明:CardPayment implements Payable 后必须提供 public pay(),此实现返回 "card"。
interface Payable {
// 作用:接口描述调用者需要的能力,一个类可以实现多个互不冲突的接口;实现方法必须满足接口可见性要求。
    String pay();
}

class CardPayment implements Payable {
    @Override
    public String pay() {
        return "card";
    }
}

System.out.println(new CardPayment().pay());
// 输出:card

抽象类:共享状态和实现骨架 ​

抽象类不能直接实例化,可以持有实例状态、构造器和部分实现;只在类型族确实共享生命周期或不变式时使用。

java
// 语义:抽象类不能直接实例化,可以持有实例状态、构造器和部分实现。
abstract class Job {
    final String run() {
        return before() + ":" + execute();
    }

    private String before() {
        return "start";
    }

    protected abstract String execute();
}

class ImportJob extends Job {
    @Override
    protected String execute() {
        return "import";
    }
}

System.out.println(new ImportJob().run());
// 输出:start:import

接口默认方法:提供可演进的默认行为 ​

default 适合小而稳定的派生行为;它不能保存每个对象的普通状态,也不能替代领域服务。

java
// 语义:default 适合小而稳定的派生行为。
interface Named {
    String name();

    default String label() {
        return "name=" + name();
    }
}

class User implements Named {
    public String name() {
        return "Ann";
    }
}

System.out.println(new User().label());
// 输出:name=Ann

接口静态方法:放置与契约相关的工具 ​

接口静态方法通过接口名调用,不会被实现类实例继承或动态分派;工具逻辑仍要保持与接口契约的高内聚。

java
// 语义:接口静态方法通过接口名调用,不会被实现类实例继承或动态分派。
interface Codes {
    static boolean valid(String value) {
        return value != null && !value.isBlank();
    }
}

System.out.println(Codes.valid("A-1"));
// 输出:true

多接口 default 冲突:显式选择实现 ​

两个接口提供同签名默认方法时,实现类必须重写并表达业务选择;不能依赖编译器猜测优先级。

java
// 语义:两个接口提供同签名默认方法时,实现类必须重写并表达业务选择。
interface Left {
    default String side() { return "left"; }
}

interface Right {
    default String side() { return "right"; }
}

class Both implements Left, Right {
    @Override
    public String side() {
        return Left.super.side() + "+" + Right.super.side();
    }
}

System.out.println(new Both().side());
// 输出:left+right

构造器注入接口:替换策略实现 ​

字段和构造器面向接口,高层流程不需要知道具体实现;测试时可以注入记录调用的假实现。

java
// 语义:字段和构造器面向接口,高层流程不需要知道具体实现。
interface Formatter {
    String format(String text);
}

class UpperFormatter implements Formatter {
    public String format(String text) {
        return text.toUpperCase();
    }
}

class Preview {
    private final Formatter formatter;

    Preview(Formatter formatter) {
        this.formatter = formatter;
    }

    String render(String text) {
        return formatter.format(text);
    }
}

System.out.println(new Preview(new UpperFormatter()).render("java"));
// 输出:JAVA

不常用但需要知道 ​

private 接口方法:复用默认实现内部逻辑 ​

私有接口方法只能被接口内部的默认或静态方法调用,适合拆出小的复用片段;它不是实现类可调用的公共 API。

java
// 输入:audit("ok") 由默认方法调用接口内的 private prefix(),把 "audit:" 与 "ok" 拼接。
interface Audited {
    default String audit(String value) {
    // 作用:私有接口方法只能被接口内部的默认或静态方法调用,适合拆出小的复用片段;它不是实现类可调用的公共 API。
        return prefix() + value;
    }

    private String prefix() {
        return "audit:";
    }
}

class Event implements Audited {}
System.out.println(new Event().audit("ok"));
// 输出:audit:ok

接口常量:隐式 public static final ​

接口字段隐式是 public static final,适合真正属于契约的常量;不要把可变配置堆成接口常量池。

java
interface Limits {
    int MAX_RETRY = 3;
// 关键变化:MAX_RETRY 接收表达式 3 的计算结果。
// 初始状态:MAX_RETRY 的初始值为 3。
}

System.out.println(Limits.MAX_RETRY);
// 输出:3

抽象类实现接口:分阶段完成契约 ​

抽象类可以把接口契约的一部分实现成模板,再把变化点留给子类;注意构造阶段不要调用可重写方法。

java
interface Step {
    String run();
}

abstract class BaseStep implements Step {
    public final String run() {
        return "base:" + detail();
    }

    protected abstract String detail();
}

class ConcreteStep extends BaseStep {
    protected String detail() {
        return "ok";
    }
}

System.out.println(new ConcreteStep().run());
// 输出:base:ok

继续阅读 ​

简单案例 ​

java
interface Notifier {
    String send(String recipient, String message);
}

final class ConsoleNotifier implements Notifier {
    @Override
    public String send(String recipient, String message) {
        return "console:" + recipient + ":" + message;
    }
}

final class SilentNotifier implements Notifier {
    @Override
    public String send(String recipient, String message) {
        return "ignored";
    }
}

final class AlertService {
    private final Notifier notifier;

    AlertService(Notifier notifier) {
        this.notifier = notifier;
    }

    String alert(String recipient) {
        return notifier.send(recipient, "paid");
    }
}

public class InterfaceDemo {
    public static void main(String[] args) {
        AlertService service = new AlertService(new ConsoleNotifier());
        System.out.println(service.alert("alice"));
        // 输出:console:alice:paid
        System.out.println(new AlertService(new SilentNotifier()).alert("test"));
        // 输出:ignored
    }
}

输出为 console:alice:paid 和 ignored。AlertService 只依赖 Notifier,替换策略不需要修改高层流程;测试时可以注入记录调用的假实现,而不必连接外部通知系统。

易混点 ​

  • 接口可以有 default、static 和 private 方法,但仍不保存每个实例的普通字段状态。
  • 抽象类可共享状态和构造流程,但单继承限制意味着它不适合作为无关能力的混合容器。
  • 两个接口的同签名 default 冲突必须由实现类重写解决,编译器不会替业务选择默认优先级。
  • 接口隔离不是把每个方法都拆成一个接口,而是让每个调用者只依赖真正需要的能力。

课后小问 ​

  1. 为什么 AlertService 的字段应声明为 Notifier 而不是 ConsoleNotifier? 答案:依赖接口后,服务只要求“能发送通知”的契约,具体输出策略可以替换。 解析:这是依赖倒置;字段写成具体类会锁定实现,测试替身和未来的短信、邮件实现都难以接入。

  2. 一个类实现两个带同签名 default 方法的接口时怎么办? 答案:实现类必须显式重写该方法,并在方法体中选择自己的行为或调用指定接口的默认实现。 解析:两个默认实现没有天然的业务优先级,显式解决能让冲突可读、可测试,也避免编译器选择不符合领域的结果。

本节小结 ​

  • 接口表达能力契约,抽象类共享紧密类型族的状态和实现骨架。
  • default 方法用于契约演进,接口不能借它保存每个实例的业务状态。
  • 依赖倒置让高层面向接口,策略实现可以注入并替换。
  • 接口隔离减少无关依赖,默认方法冲突必须由实现类明确处理。

快速回顾 ​

  • 能根据共享状态、单继承限制和能力契约选择接口或抽象类。
  • 能解释 default、static、private 接口方法的边界。
  • 能用构造器注入一个可替换策略实现。
  • 能说出两个接口默认方法冲突的解决方式。