抽象类与接口对比
飞翔科技·架构评审:架构师白歌在设计评审文档里画了一张表格,将抽象类与接口的差异逐行列出。部门主管大翔看完后说:"这张表贴到技术 Wiki 上,让所有人都背下来。" 后端开发小崔如获至宝,这张表陪他度过了无数场面试。
全面对比表
语法与特性对比
| 对比维度 | 抽象类(abstract class) | 接口(interface) |
|---|---|---|
| 关键字 | abstract class | interface |
| 实例化 | ❌ 不能 new | ❌ 不能 new |
| 构造方法 | ✅ 可以有(供子类 super() 调用) | ❌ 不能有 |
| 成员变量 | ✅ 可以有任意类型的实例变量 | ❌ 只能有常量(public static final) |
| 抽象方法 | ✅ 可以有,用 abstract 修饰 | ✅ 可以有,默认 public abstract |
| 具体方法 | ✅ 可以有 | ❌ JDK 7 及之前不能有;✅ JDK 8 可以有 default 方法和 static 方法 |
| 方法访问修饰符 | 任意(public / protected / private / 默认) | 只能是 public(default 和 static 方法也是 public) |
| 继承/实现 | 单继承(一个类只能 extends 一个抽象类) | 多实现(一个类可以 implements 多个接口) |
| 设计关系 | is-a 关系("是一个") | can-do 关系("能做某事") |
| 访问修饰符(类本身) | public 或默认(包级可见) | public 或默认(包级可见) |
final 修饰 | 不能与 final 共存(抽象类必须被继承) | 不能与 final 共存(接口必须被实现) |
| 静态代码块 | ✅ 可以有 static { } | ❌ 不能有 |
包含 main 方法 | ✅ 可以,可通过 java AbstractClass 运行(但不能实例化自身) | ✅ JDK 8 起可以(static void main),可通过 java InterfaceName 运行 |
设计哲学对比
何时用抽象类
场景特征
抽象类适合以下场景:
- is-a 关系明确:子类确实"是"父类的一种具体类型
- 共享状态:多个子类有相同的字段(成员变量),需要复用初始化逻辑
- 模板方法模式:需要定义骨架算法,将可变步骤留给子类
- 非 public 方法:需要
protected或包级可见的方法,供子类调用但不对外暴露 - 构造逻辑复用:子类初始化流程高度一致,可以抽取到父类构造方法中
飞翔科技实战:Employee 体系
判断逻辑:"全时员工是一个员工吗?实习生是一个员工吗?"——答案都是"是"。因此抽象类是正确的选择。
何时用接口
场景特征
接口适合以下场景:
- can-do 关系:类需要"具备某种能力",但这种能力跨越不同类层次结构
- 多继承需求:一个类需要从多个来源继承行为契约
- 面向接口编程:调用方只依赖接口,不依赖具体实现——实现可自由替换
- 跨层级能力:完全不相关的类需要共享相同的行为签名(如
Comparable接口——String、Integer、Student都实现它) - 标记接口:如
Serializable、Cloneable,不需要任何方法,只用来标记某种能力 - 函数式接口:配合 Lambda 表达式使用(JDK 8 核心特性)
飞翔科技实战:Payable + Printable 跨层级能力
判断逻辑:"员工'能被支付'吗?发票'能被支付'吗?合同'能被支付'吗?"——答案都是"能"。它们之间没有 is-a 关系(发票不是一个员工),但它们共享 can-do 能力。因此接口是正确的选择。
设计模式视角
模板方法模式 → 抽象类
// 抽象类定义算法骨架
public abstract class DataExporter {
// 模板方法:final 防止篡改
public final void export() {
openConnection();
fetchData();
formatData();
writeOutput();
closeConnection();
}
protected abstract void fetchData(); // 抽象:子类实现
protected abstract void formatData(); // 抽象:子类实现
private void openConnection() { /* 通用实现 */ }
private void writeOutput() { /* 通用实现 */ }
private void closeConnection(){ /* 通用实现 */ }
}
策略模式 → 接口
// 接口定义策略
public interface PaymentStrategy {
void pay(double amount);
}
// 策略一
public class CreditCardStrategy implements PaymentStrategy {
public void pay(double amount) { /* 信用卡支付 */ }
}
// 策略二
public class AlipayStrategy implements PaymentStrategy {
public void pay(double amount) { /* 支付宝支付 */ }
}
// 上下文类——面向接口编程
public class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy; // 运行时切换策略
}
public void executePayment(double amount) {
strategy.pay(amount);
}
}
适配器模式 → 接口 + 抽象类组合
// JDK 中的经典案例:MouseAdapter
// MouseListener 是接口(5个方法)
public interface MouseListener {
void mouseClicked(MouseEvent e);
void mousePressed(MouseEvent e);
void mouseReleased(MouseEvent e);
void mouseEntered(MouseEvent e);
void mouseExited(MouseEvent e);
}
// MouseAdapter 是抽象类——为所有方法提供空实现
public abstract class MouseAdapter implements MouseListener {
public void mouseClicked(MouseEvent e) { }
public void mousePressed(MouseEvent e) { }
public void mouseReleased(MouseEvent e) { }
public void mouseEntered(MouseEvent e) { }
public void mouseExited(MouseEvent e) { }
}
// 用户只需继承 MouseAdapter,重写关心的那一个方法
new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("点击了!");
}
};
这是抽象类与接口协同工作的经典范例:接口定义契约,抽象类提供便利的默认实现。
JDK 8 对接口设计的革命性改变
改变之前(JDK 7)
// JDK 7 时代:接口只能有抽象方法和常量
public interface Payable {
double MIN_AMOUNT = 0.01; // 常量
void pay(double amount); // 抽象方法
String getPaymentMethod(); // 抽象方法
}
痛点:如果要给 Payable 新增一个方法,所有实现类都必须修改代码。
改变之后(JDK 8)
// JDK 8:接口可以有 default 方法和 static 方法
public interface Payable {
// ---- 抽象方法(子类必须实现) ----
void pay(double amount);
String getPaymentMethod();
// ---- default 方法(提供默认实现,子类可选重写) ----
default void printReceipt(double amount) {
System.out.println("支付金额:" + amount);
System.out.println("支付方式:" + getPaymentMethod());
}
default void validateAmount(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}
}
// ---- static 方法(工具/工厂方法) ----
static Payable of(String type, String account) {
switch (type) {
case "BANK": return new BankPayment("默认银行", account);
case "ALIPAY": return new AlipayPayment(account);
default: throw new IllegalArgumentException("未知类型: " + type);
}
}
static boolean isValid(double amount) {
return amount > 0 && amount < 1_000_000;
}
}
JDK 8 default 方法的深远影响
经典案例:Java 标准库中的 default 方法
| 接口 | default 方法示例 | 作用 |
|---|---|---|
java.util.Collection | stream()、parallelStream()、removeIf() | 为所有集合类统一提供 Stream API |
java.util.Iterator | forEachRemaining()、remove() | 提供默认的遍历行为 |
java.util.Comparator | reversed()、thenComparing() | 链式比较器组合 |
java.util.function.Function | compose()、andThen() | 函数组合 |
java.util.function.Predicate | and()、or()、negate() | 谓词逻辑组合 |
决策流程图
易错场景
反例一:把 is-a 关系强行用接口实现
// ❌ 错误:所有员工都用接口,丢失了共享状态的优势
public interface Employee {
String getName(); // 每个实现类都要自己写 getName
String getId(); // 每个实现类都要自己写 getId
double calculateSalary();
}
// 问题:name 和 id 的存储、getter 实现完全重复
public class FullTimeEmployee implements Employee {
private String name; // 重复
private String id; // 重复
// + getter 重复实现
}
public class Intern implements Employee {
private String name; // 重复
private String id; // 重复
// + getter 重复实现
}
纠正:有共享状态时,应该使用抽象类。
反例二:为了"多用几个方法"而滥用抽象类
// ❌ 错误:Invoice 不是 Employee,却继承了 Employee 只是为了复用字段
public class Invoice extends Employee {
// Invoice(发票)不是一个 Employee(员工),is-a 不成立!
private String invoiceNo;
}
纠正:用接口声明 Payable 能力:
// ✅ 正确
public class Invoice implements Payable {
private String invoiceNo;
// 实现 Payable 的方法
}
反例三:JDK 8 之后把所有东西都写成接口
// ❌ 误区:因为 JDK 8 接口有 default 方法,就把所有抽象类改成接口
public interface Employee {
String name; // ❌ 这实际是 public static final 常量,不是实例变量!
default String getName() {
return name; // ❌ name 是常量,所有"实例"共享同一个值!
}
}
纠正:接口仍然不能有实例变量。需要状态时,必须使用抽象类(或普通类)。
面试考点汇总
Q1:抽象类和接口的核心区别是什么?请用表格说明。
(参考上文"语法与特性对比"表格。面试时重点答三点:① 构造方法有无 ② 成员变量限制 ③ 单继承 vs 多实现。)
Q2:JDK 8 之后接口有了 default 方法,抽象类还有存在的必要吗?
有必要。 default 方法无法改变接口的三个根本限制:
- 接口不能有实例状态(所有变量都是
public static final)- 接口不能有构造方法,无法做初始化逻辑复用
- 接口方法必须是 public,不能用
protected限制子类可见性当需要共享成员变量、构造逻辑复用、或非 public 方法时,必须使用抽象类。
Q3:什么时候用抽象类,什么时候用接口?举实际例子。
- 抽象类:
HttpServlet(共享ServletConfig状态,定义doGet/doPost模板)、InputStream(共享读取状态,定义read()模板)- 接口:
Comparable(跨所有类的比较能力)、Runnable(跨所有类的线程执行能力)、Serializable(标记接口)- 组合:
MouseListener(接口定义契约)+MouseAdapter(抽象类提供空实现)= 经典组合模式
Q4:抽象类中是否可以没有抽象方法?有什么用?
可以。 这种抽象类叫"工具性抽象类",例如
java.awt.event.MouseAdapter。它的作用是提供接口的默认空实现,让子类只重写需要的方法,而不是被迫实现所有方法。这是接口 + 抽象类适配器模式的经典应用。
Q5:interface 中的 static 方法能被实现类继承吗?
不能。 接口的 static 方法只能通过
InterfaceName.method()调用,不能通过实现类的类名或实例调用。这与类的 static 方法不同——类的 static 方法可被子类继承。这是 JDK 8 语言规范(JLS 9.4)的明确规定,目的是避免多接口实现时的静态方法调用歧义。
Q6:为什么接口中不能定义 protected 方法?
接口的核心语义是"对外承诺的能力"。
protected意味着"只对子类可见",这与接口的"公开契约"理念相违背。如果一个方法只对子类有意义,不对外暴露,那么它应该定义在抽象类中,而不是接口中。
Q7:请解释"面向接口编程"(Program to Interface)的含义及其优势。
含义:代码依赖抽象(接口)而非具体实现类。例如
SalaryPrinter依赖Payable接口而非BankPayment具体类。优势:
- 解耦:更换支付方式(银行→支付宝)无需修改调用方代码
- 可测试:单元测试时可注入 Mock 实现
- 可扩展:新增支付方式只需新建实现类,符合开闭原则
- 并行开发:接口定义后,调用方和实现方可独立开发
本章总结
白歌的忠告:"抽象类解决的是'我们是什么'的问题——提取共性,定义模板。接口解决的是'我们能做什么'的问题——声明能力,解耦依赖。两者不是替代关系,而是互补关系。真正优秀的架构,是让抽象类和接口各司其职,协同工作。"