接口与抽象类
学习目标
- 区分接口、抽象类和普通类的职责与扩展边界。
- 理解
default、static、private接口方法及多接口冲突解决。 - 用依赖倒置和接口隔离设计可替换策略,并编写多态测试案例。
核心知识点
专业术语
- 接口(interface):面向调用者的能力契约;一个类可以实现多个接口。
- 抽象类(abstract class):不能直接实例化的基类,可共享实例状态、构造流程和部分实现。
- default 方法:接口提供的默认实例实现,用于演进契约,但不能保存每个对象的业务状态。
- 接口隔离原则(ISP):调用者不应被迫依赖它不使用的方法,应把大契约拆成小能力。
- 依赖倒置原则(DIP):高层策略依赖抽象,具体实现通过构造器或工厂注入。
白话解释与边界
接口回答“对象能做什么”,抽象类回答“同一族对象有哪些共用骨架”。接口字段隐含为 public static final,抽象类可以有构造器和受保护状态,但 Java 类只能直接继承一个父类。default 适合向后兼容的小行为,若两个接口提供同签名默认方法,实现类必须显式重写解决冲突;复杂领域状态不应藏进默认方法。
策略类只依赖接口,就能把支付、通知或格式化实现替换成真实服务、测试替身或新版本。接口越小,替代实现越容易;抽象类更适合需要共享校验和生命周期的紧密类型族。不要为了“可扩展”把所有方法都塞进一个接口。
常用用法
interface/implements:声明并实现能力契约
接口描述调用者需要的能力,一个类可以实现多个互不冲突的接口;实现方法必须满足接口可见性要求。
// 说明: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抽象类:共享状态和实现骨架
抽象类不能直接实例化,可以持有实例状态、构造器和部分实现;只在类型族确实共享生命周期或不变式时使用。
// 语义:抽象类不能直接实例化,可以持有实例状态、构造器和部分实现。
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 适合小而稳定的派生行为;它不能保存每个对象的普通状态,也不能替代领域服务。
// 语义: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接口静态方法:放置与契约相关的工具
接口静态方法通过接口名调用,不会被实现类实例继承或动态分派;工具逻辑仍要保持与接口契约的高内聚。
// 语义:接口静态方法通过接口名调用,不会被实现类实例继承或动态分派。
interface Codes {
static boolean valid(String value) {
return value != null && !value.isBlank();
}
}
System.out.println(Codes.valid("A-1"));
// 输出:true多接口 default 冲突:显式选择实现
两个接口提供同签名默认方法时,实现类必须重写并表达业务选择;不能依赖编译器猜测优先级。
// 语义:两个接口提供同签名默认方法时,实现类必须重写并表达业务选择。
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构造器注入接口:替换策略实现
字段和构造器面向接口,高层流程不需要知道具体实现;测试时可以注入记录调用的假实现。
// 语义:字段和构造器面向接口,高层流程不需要知道具体实现。
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。
// 输入: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,适合真正属于契约的常量;不要把可变配置堆成接口常量池。
interface Limits {
int MAX_RETRY = 3;
// 关键变化:MAX_RETRY 接收表达式 3 的计算结果。
// 初始状态:MAX_RETRY 的初始值为 3。
}
System.out.println(Limits.MAX_RETRY);
// 输出:3抽象类实现接口:分阶段完成契约
抽象类可以把接口契约的一部分实现成模板,再把变化点留给子类;注意构造阶段不要调用可重写方法。
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继续阅读
- 封装、继承与多态:继承契约、动态分派与组合边界。
- 构造器与初始化顺序:接口实现类和抽象基类的初始化过程。
static、final与代码组织:接口静态方法和不可变成员的组织方式。
简单案例
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冲突必须由实现类重写解决,编译器不会替业务选择默认优先级。 - 接口隔离不是把每个方法都拆成一个接口,而是让每个调用者只依赖真正需要的能力。
课后小问
为什么
AlertService的字段应声明为Notifier而不是ConsoleNotifier? 答案:依赖接口后,服务只要求“能发送通知”的契约,具体输出策略可以替换。 解析:这是依赖倒置;字段写成具体类会锁定实现,测试替身和未来的短信、邮件实现都难以接入。一个类实现两个带同签名 default 方法的接口时怎么办? 答案:实现类必须显式重写该方法,并在方法体中选择自己的行为或调用指定接口的默认实现。 解析:两个默认实现没有天然的业务优先级,显式解决能让冲突可读、可测试,也避免编译器选择不符合领域的结果。
本节小结
- 接口表达能力契约,抽象类共享紧密类型族的状态和实现骨架。
- default 方法用于契约演进,接口不能借它保存每个实例的业务状态。
- 依赖倒置让高层面向接口,策略实现可以注入并替换。
- 接口隔离减少无关依赖,默认方法冲突必须由实现类明确处理。
快速回顾
- 能根据共享状态、单继承限制和能力契约选择接口或抽象类。
- 能解释 default、static、private 接口方法的边界。
- 能用构造器注入一个可替换策略实现。
- 能说出两个接口默认方法冲突的解决方式。
阿源的知识库