02.2 — 抽象类与接口
定位: Java 中"抽象"的两种工具 — 模板 vs 契约,在不同场景下服务于不同的设计目的 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 设计动机差异、JDK8+ 接口变化、如何选择
一、这是什么?为什么需要它?
抽象类:模板方法模式
java
abstract class HttpServlet {
// 模板方法 — 定义请求处理流程
public void service(Request req, Response res) {
if (req.getMethod().equals("GET")) {
doGet(req, res); // 子类实现
} else if (req.getMethod().equals("POST")) {
doPost(req, res); // 子类实现
}
}
protected abstract void doGet(Request req, Response res);
protected abstract void doPost(Request req, Response res);
}为什么需要抽象类? — 复用骨架,子类只需填充差异部分。
没有抽象类会怎样?
每个 Servlet 都要自己实现 service() 方法
→ 每个类都重复写 GET/POST 判断逻辑
→ 如果请求处理流程变化(加 PUT),所有类都要改
抽象类的价值:
定义算法的骨架,子类只提供"变体"
符合"模板方法模式" — 好莱坞原则:不要打电话给我们,我们会打给你接口:契约设计
java
// 接口 = 能力契约
interface Flyable {
void fly(); // 说"能飞"就行,不管怎么飞
}
// 不同的实现
class Bird implements Flyable {
public void fly() { System.out.println("拍翅膀"); }
}
class Airplane implements Flyable {
public void fly() { System.out.println("喷气引擎"); }
}为什么需要接口? — 跨继承树的共性抽象。
Bird extends Animal
Airplane extends Machine
Bird 和 Airplane 没有继承关系
但都有 fly() 能力 → 用接口跨越继承树二、原理拆解
2.1 核心差异对比
| 维度 | 抽象类 | 接口 (JDK8+) |
|---|---|---|
| 核心语义 | "是什么" (is-a) | "能做什么" (can-do) |
| 构造方法 | 可以有 | 不能有 |
| 状态字段 | 可以有 (任意) | 不能有 (仅 public static final) |
| 方法实现 | 可抽象可具体 | default 方法、static 方法 |
| 多重继承 | 单继承 | 多实现 |
| 访问修饰符 | 任意 | public (JDK9 有 private 方法) |
| 演进兼容性 | 加抽象方法→所有子类需改 | default 方法→实现类不感知 |
2.2 JDK8+ 接口进化
为什么 JDK8 引入了 default 方法?
问题:Collection 接口想加 stream() 方法
如果加 abstract stream() → 所有实现类都要改!
解决方案:default 方法
interface Collection<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
→ 已有实现类自动继承 default 方法,无需修改default 方法的多重继承冲突解决:
java
interface A {
default void foo() { System.out.println("A"); }
}
interface B {
default void foo() { System.out.println("B"); }
}
class C implements A, B {
// 必须重写!否则编译错误
@Override
public void foo() {
A.super.foo(); // 指定用 A 的实现
}
}规则:类优先于接口
- 如果子类有具体实现 → 用子类的
- 如果子类没有 → 找父类中最具体的 default 方法
- 冲突时 → 必须手动解决
2.3 选择决策树
是否需要共享状态(字段)?
├── 是 → 抽象类
└── 否 → 核心是"是什么"还是"能做什么"?
├── is-a 关系 → 抽象类
└── can-do 能力 → 接口
需要多重继承?
├── 是 → 接口(必须)
└── 否 → 看是否需要构造函数/状态
├── 需要 → 抽象类
└── 不需要 → 接口(更灵活)三、图解全景
┌─────────────────────────────────────────────────────────────┐
│ 抽象类 vs 接口 内存结构对比 │
│ │
│ 抽象类: │
│ abstract class Shape { │
│ int x, y; ← 状态字段 │
│ abstract void draw(); ← 抽象方法 │
│ void move(int dx, int dy) { ← 具体方法 │
│ x += dx; y += dy; │
│ } │
│ } │
│ │
│ 接口: │
│ interface Drawable { │
│ void draw(); ← 抽象方法 │
│ default void preview() { ... } ← 默认方法 │
│ } │
│ │
│ 内存视角: │
│ 抽象类子类: Object Header + Shape 字段 + 子类字段 + vtable │
│ 接口实现类: Object Header + 父类字段 + 子类字段 + vtable │
│ (接口的 default 方法在 vtable 中占据槽位) │
└─────────────────────────────────────────────────────────────┘四、实战验证
4.1 抽象类 vs 接口的演进对比
java
// JDK 7 风格
interface OldStyle {
void method(); // 只有抽象方法
}
// JDK 8 风格
interface NewStyle {
void method();
default void log(String msg) {
System.out.println("[LOG] " + msg);
}
static NewStyle create() {
return () -> System.out.println("Impl");
}
}4.2 典型选择场景
java
// ✅ 用抽象类:模板方法
abstract class AbstractAnimal {
protected String name;
AbstractAnimal(String name) { this.name = name; }
abstract void makeSound();
void introduce() {
System.out.println("I'm " + name);
makeSound();
}
}
// ✅ 用户接口:契约
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
// ✅ 同时使用
class Duck extends AbstractAnimal implements Flyable, Swimmable {
Duck(String name) { super(name); }
void makeSound() { System.out.println("Quack!"); }
public void fly() { System.out.println("Flying..."); }
public void swim() { System.out.println("Swimming..."); }
}五、面试视角
| 追问 | 答案要点 |
|---|---|
| 抽象类和接口的根本区别? | 语义:is-a vs can-do;状态:支持字段 vs 不支持;继承:单 vs 多 |
| JDK8 为什么引入 default 方法? | 接口演进兼容性:在已有接口加方法而不破坏已有实现 |
| 接口中的变量为什么是 public static final? | 接口不能有实例状态,static final 表示常量级契约 |
| JDK9 接口的 private 方法有什么用? | 提取 default 方法的公共逻辑,但不能对外暴露 |
| 什么时候用接口而非抽象类? | 需要多重继承、纯能力契约、不关心状态时 |
📚 相关链接
- **三大特性(封装继承多态)** — 多态与接口的关系
- **注解体系** — 注解也是一种接口
- **注解体系速查** — 接口在框架中的大量应用
- ← 返回 **OOP索引**