8.4 抽象类、接口与 Object
前置知识:继承、重写和动态分派
预计时间:35 分钟
完成标志:能根据状态共享、实现复用和调用方契约选择抽象类、接口或组合
通用 MessageSender 本身并没有实际运输方式。若它提供一个看似能工作的默认 send,子类忘记重写时,错误只能到运行时才暴露。与此同时,工作人员已经继承 Worker,却也需要加入“可发送”调度契约。
1. 抽象类保存受控骨架
本页独立定义返回 String 的发送接口,与前页返回 void 的版本分开编译。示例只构造演示文本,不接入发送服务。
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();
}抽象类不能直接实例化,却可以拥有构造方法、实例状态、具体方法和抽象方法。构造方法用于初始化将来子类对象中的父类部分。
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. 接口定义调用方需要的能力
调度中心只关心“能发送”:
interface Sendable {
String send();
}接口抽象方法隐式为 public abstract,实现时不能降低可见性:
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 接口可以包含实现
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 方法能在某些情况下演进接口,但不是“加方法永不破坏实现”的保证。新默认方法可能与实现类、父类或其他接口的方法发生签名冲突,也可能改变既有调用的解析结果。
默认方法冲突
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。数组也是对象。常用方法包括:
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 用于面向开发者的可读表示,不应默认包含密码、令牌或个人数据,也不应作为稳定序列化格式。
@Override
public String toString() {
return "SmsSender{messageLength=" + message().length() + "}";
}6. 接口引用仍指向普通对象
Sendable sender = new ConsoleChannel("今晚开会");
System.out.println(sender.send());接口不能 new,但接口变量可以引用实现对象。实例方法仍按实际类动态分派。接口的价值在于缩小调用方依赖,而不是创建另一类特殊对象。
专业速查
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 构造方法 | 有 | 无 |
| 实例状态 | 有 | 无 |
| 抽象方法 | 有 | 有 |
| 具体实例方法 | 有 | default/private |
| static 方法 | 有 | 有 |
| 一个类可直接使用数量 | extends 一个类 | implements 多个接口 |
离开这一页前
调度中心只需要发送能力时让它依赖 Sendable;确实要共享受控状态与骨架时再引入抽象类。编译两个默认方法冲突的接口实现,并显式选择 Audited.super 与 Tracked.super。重写值相等还必须同步维护 hashCode,默认对象字符串也不能当作地址或序列化格式。
规范入口:JLS §9:接口。