跳到内容

第 1 卷 / 第 8 章 / 第 4 课

8.4 抽象类、接口与 Object ​

前置知识:继承、重写和动态分派

预计时间:35 分钟

完成标志:能根据状态共享、实现复用和调用方契约选择抽象类、接口或组合

通用 MessageSender 本身并没有实际运输方式。若它提供一个看似能工作的默认 send,子类忘记重写时,错误只能到运行时才暴露。与此同时,工作人员已经继承 Worker,却也需要加入“可发送”调度契约。

1. 抽象类保存受控骨架 ​

本页独立定义返回 String 的发送接口,与前页返回 void 的版本分开编译。示例只构造演示文本,不接入发送服务。

java
import java.util.Objects;

abstract class MessageSender {
    private final String message;

    MessageSender(String message) {
        this.message = Objects.requireNonNull(message, "message");
        if (message.isBlank()) {
            throw new IllegalArgumentException("blank message");
        }
    }

    protected final String message() {
        return message;
    }

    protected final String formattedMessage() {
        return "消息:" + message;
    }

    public abstract String send();
}

抽象类不能直接实例化,却可以拥有构造方法、实例状态、具体方法和抽象方法。构造方法用于初始化将来子类对象中的父类部分。

java
final class SmsSender extends MessageSender {
    SmsSender(String message) {
        super(message);
    }

    @Override
    public String send() {
        return formattedMessage() + ";demo SMS channel";
    }
}

非抽象子类必须实现继承到的所有抽象方法,否则编译失败。抽象方法没有方法体,也不能同时是 private、static 或 final,因为这些修饰与“由子类覆盖”矛盾。

基类把状态保持 private,只开放 protected final 操作,避免子类直接依赖字段表示。这仍是继承协议:修改 protected 方法的语义也可能破坏子类。

2. 接口定义调用方需要的能力 ​

调度中心只关心“能发送”:

java
interface Sendable {
    String send();
}

接口抽象方法隐式为 public abstract,实现时不能降低可见性:

java
final class ConsoleChannel implements Sendable {
    private final String message;

    ConsoleChannel(String message) {
        this.message = java.util.Objects.requireNonNull(message);
    }

    @Override
    public String send() {
        return "控制台客户端:" + message;
    }
}

一个类只能直接扩展一个类,却可实现多个接口。接口还可扩展多个接口。这允许 Auditor extends Worker implements Sendable, Trackable,但多个契约仍可能在方法语义或默认实现上冲突。

接口没有构造方法和实例字段。接口字段隐式为 public static final,由所有实现对象共享;final 只限制字段再次赋值,不能让它所指的列表或数组自动不可变。只有满足常量表达式等条件的基本类型或 String 字段才是编译期常量,因此不要用接口字段保存每个对象的状态。

3. Java 17 接口可以包含实现 ​

java
interface Sendable {
    String send();

    default String channelName() {
        return normalize("unknown");
    }

    static boolean isValid(String message) {
        return message != null && !message.isBlank();
    }

    private static String normalize(String value) {
        return value.trim().toLowerCase(java.util.Locale.ROOT);
    }
}
  • default 方法提供实例默认实现,可以被覆盖。
  • static 方法属于接口,通过 Sendable.isValid(...) 调用,不被实现类继承成实例方法。
  • Java 9 起接口可用 private 方法复用内部实现。

default 方法能在某些情况下演进接口,但不是“加方法永不破坏实现”的保证。新默认方法可能与实现类、父类或其他接口的方法发生签名冲突,也可能改变既有调用的解析结果。

默认方法冲突 ​

java
interface Audited {
    default String label() { return "audit"; }
}

interface Tracked {
    default String label() { return "track"; }
}

final class Auditor implements Audited, Tracked {
    @Override
    public String label() {
        return Audited.super.label() + "+" + Tracked.super.label();
    }
}

两个无继承关系的接口提供同签名默认方法时,实现类必须解决冲突。一般解析原则包括“类方法优先”和“更具体接口优先”;复杂层次应以编译器和 JLS 规则验证,避免靠口诀猜测。

4. 抽象类、接口和组合怎样选 ​

需求首选
调用方只需要一项能力契约接口
多个无关类型需要同一能力接口
子类共享稳定状态和受控骨架抽象类
只想复用协作者实现组合/委托
需要多个可替换策略接口 + 组合

“共享字段就用抽象类”也只是起点。若类型并不满足同一替换契约,把状态放进独立组件再组合通常更稳妥。公共 API 常让调用方依赖接口,内部实现是否再共享抽象基类则是另一项决定。

5. Object 是类层次的根 ​

除 Object 自身外,每个 Java 类都直接或间接扩展 java.lang.Object。数组也是对象。常用方法包括:

java
Object value = new SmsSender("天气转晴");
System.out.println(value.toString());
System.out.println(value.getClass().getName());
System.out.println(value.equals(value));
System.out.println(value.hashCode());

Object 默认 equals 使用对象身份语义,默认 hashCode 与身份相关,但返回值不是承诺的内存地址。重写按值相等时必须同时维护 equals 与 hashCode 契约,否则 HashMap/HashSet 等集合会出现错误行为。

toString 用于面向开发者的可读表示,不应默认包含密码、令牌或个人数据,也不应作为稳定序列化格式。

java
@Override
public String toString() {
    return "SmsSender{messageLength=" + message().length() + "}";
}

6. 接口引用仍指向普通对象 ​

java
Sendable sender = new ConsoleChannel("今晚开会");
System.out.println(sender.send());

接口不能 new,但接口变量可以引用实现对象。实例方法仍按实际类动态分派。接口的价值在于缩小调用方依赖,而不是创建另一类特殊对象。

专业速查 ​

特性抽象类接口
构造方法有无
实例状态有无
抽象方法有有
具体实例方法有default/private
static 方法有有
一个类可直接使用数量extends 一个类implements 多个接口

离开这一页前 ​

调度中心只需要发送能力时让它依赖 Sendable;确实要共享受控状态与骨架时再引入抽象类。编译两个默认方法冲突的接口实现,并显式选择 Audited.super 与 Tracked.super。重写值相等还必须同步维护 hashCode,默认对象字符串也不能当作地址或序列化格式。

→ 下一步:继承、接口与多态实验

规范入口:JLS §9:接口。

Built with VitePress | Software Systems Atlas