在上一篇文章中,我们了解了抽象和接口思想。抽象和接口思想是基于"软件是持续变化的"、"复杂系统需要解耦合"的公理推导出的重要思想。抽象剥离了变化,形成可复用的稳定层;接口将变化隔离,让模块之间不互相影响。在这篇文章,我们将结合Java语言深入学习抽象和接口,将思想落地。
一、Java语言中,抽象和接口的区别是什么?
两者都是"不能直接 new"的类型,但本质完全不同
| 对比维度 | abstract class(抽象类) | interface(接口) |
|---|---|---|
| 根本目的 | 复用 | 隔离(解耦) |
| 核心回答 | is-a,"你是什么" | can-do,"你能做什么" |
| 字段 | 可以有字段:String name; |
没有字段 |
| 普通方法 | 有方法体:void breathe() { ... } |
无,只有方法签名 |
| 抽象方法 | abstract void move(); |
void move(); // 全部默认 public abstract |
| 本质 | 既有"骨架"也有"血肉" | 只有"合同",没有"实现" |
| 继承/实现 | 单继承:class Dog extends Animal |
多实现:class Dog implements Movable, Eatable |
共同点:都不能直接实例化(不能 new),都要靠子类/实现类来"落地"。
二、Java语言中,什么时候用抽象类?什么时候用接口?
| 判断维度 | 抽象类 | 接口 |
|---|---|---|
| 核心场景 | 子类之间有"共同基因" | 类之间没有"血缘关系" |
| 触发条件 | 有共享的字段/状态(如:所有动物都有 name, age) | 1、只有"能力"契约(如:Dog、Car、Plane 都能 move) 2、接口预期有 ≥ 2 个实现类? → 用接口;只有 1 个实现类 → 接口可能是过度设计。 |
| 共享代码 | ✅ 有,可以复用 | ❌ 没有东西可共享 |
| 代码 | abstract class Animal { protected String name; // 共享状态 void breathe() { ... } // 共享行为 abstract void move(); // 子类各自实现} |
interface Movable { void move(); // 只签合同 void stop(); // 不管实现} |
1、代码实战:
我们用抽象和接口实现同一个功能来感受下:设计一个数据访问层 DataRepository,支持 MySQL、PostgreSQL 和内存测试版。
方案一:纯接口
interface DataRepository {
User findById(Long id);
void save(User user);
}
// 每个实现都从头写,连接池代码各写一遍
class MySQLRepository implements DataRepository {
private ConnectionPool pool;
void initPool() { /* 30行连接池代码 */ }
public User findById(Long id) { /* SQL + 连接池操作 */ }
public void save(User user) { /* SQL + 连接池操作 */ }
}
class PostgresRepository implements DataRepository {
private ConnectionPool pool;
void initPool() { /* 一模一样的30行连接池代码 ← 重复! */ }
public User findById(Long id) { /* SQL + 连接池操作 */ }
public void save(User user) { /* SQL + 连接池操作 */ }
}
问题:连接池代码重复了。MySQL 和 Postgres 的差异只在 SQL 方言,连接管理完全一样。
方案二:接口 + 抽象类
// 接口:定义"调用者需要的能力"
interface DataRepository {
User findById(Long id);
void save(User user);
}
// 抽象类:提取"所有数据库都需要的公共代码"
abstract class BaseRepository implements DataRepository {
protected ConnectionPool pool; // ← 共享状态
protected void initPool(String url) { // ← 共享行为,只写一次
this.pool = new ConnectionPool(url);
}
protected Object executeQuery(String sql) { // ← 共享行为
// 连接池取连接、执行、释放 ------ 所有数据库都一样
}
// findById 和 save 留给子类,因为 SQL 方言不同
public abstract User findById(Long id);
public abstract void save(User user);
}
// 子类只写自己特有的部分
class MySQLRepository extends BaseRepository {
public User findById(Long id) {
return executeQuery("SELECT * FROM users WHERE id = ? LIMIT 1");
}
public void save(User user) {
execute("INSERT INTO users VALUES (?) ON DUPLICATE KEY UPDATE ...");
}
}
class PostgresRepository extends BaseRepository {
public User findById(Long id) {
return executeQuery("SELECT * FROM users WHERE id = $1 LIMIT 1");
}
public void save(User user) {
execute("INSERT INTO users VALUES ($1) ON CONFLICT ...");
}
}
为什么这个接口只暴露两个方法?因为调用者(业务层)只需要"查"和"存",不需要知道连接池、事务、SQL 方言------那些是实现者的私事。
2、接口和抽象类各司其职:
DataRepository (接口)
│ 定义:调用者需要 findById 和 save
│
▼
BaseRepository (抽象类)
│ 提供:连接池管理、SQL 执行框架 ------ 所有数据库共享
│
├── MySQLRepository 只写 MySQL 特有的 SQL
└── PostgresRepository 只写 Postgres 特有的 SQL
三、总结:五条铁律
| 判断条件 | 用抽象类 | 用接口 |
|---|---|---|
| 侧重复用还是隔离? | 侧重复用 | 侧重隔离(解耦) |
| 实现者之间有共享代码? | ✅ 提取共享代码 | ❌ 没有东西可共享 |
| 实现者属于同一"家族"? | ✅ 都是同一物种 | ❌ 跨物种(Dog/Car/Plane) |
| 预期有多个实现类? | 不要求 | 必须>=2,否则无意义 |
| 需要多继承? | ❌ Java 不支持 | ✅ 可以实现多个接口 |
抽象类和接口是抽象思想在 Java 语法层面的两个工具。Java 的 interface 是接口思想在语法层的一种实现形式,接口思想本身远比关键字更广。老师多次说过:"接口不局限于前后端 API------后端每一层类的方法、前端 MVVM 的绑定、甚至内部类的引用关系,都叫接口"。
最后引用老师的比喻作为文章的结束:
「花的鲜艳不是为了好看,是为了传粉。同理,接口不只是提供服务,更是引导外部参与者共同完成价值创造。蜜蜂、风、鸟都是参与对象,体现了『万物皆可协作』的设计理念。」
四、常见误区与 FAQ
1. 一个类只能继承一个抽象类,但可以实现多个接口,这是否意味着接口总是更好的选择?
误区澄清 :不是的。这个特性差异只是工具特性,不是选择标准。接口的"多实现"优势主要体现在横向扩展能力 上,适合定义跨领域、跨层级的契约。而抽象类的"单继承"限制恰恰体现了纵向继承关系的严谨性,适合在紧密相关的类族中建立共享基础。
选择建议:
- 如果多个类需要共享具体实现代码(如字段、方法体),且它们属于同一逻辑"家族",用抽象类。
- 如果只是定义行为契约,且这些契约可能被完全不相关的类实现,用接口。
- 实际项目中,常见模式是:接口定义核心能力,抽象类提供默认实现。
2. 既然接口可以定义默认方法(Java 8+),那抽象类是不是可以被淘汰了?
误区澄清:不能淘汰。接口的默认方法虽然提供了部分实现能力,但仍有重要限制:
- 不能定义实例字段:接口的默认方法只能操作接口中定义的常量或参数,无法维护对象状态。
- 不能定义构造器:无法在创建对象时执行初始化逻辑。
- 多重继承冲突:当一个类实现多个接口,且这些接口有同名默认方法时,必须显式重写解决冲突。
选择建议:
- 需要共享状态(字段) 或构造逻辑时,仍然需要抽象类。
- 接口默认方法更适合提供工具方法 或向后兼容的扩展。
3. 设计时应该"面向接口编程",那是不是所有类都应该先定义接口?
误区澄清 :这是对"面向接口编程"的过度简化。该原则的核心是依赖抽象而非具体实现,目的是降低耦合、提高可测试性。只有一个类实现此接口时,没有必要定义接口,尽量不要过度设计。
实践建议:
- 外部依赖:模块间、层间、系统间交互应通过接口定义契约。
- 内部实现:同一模块内紧密协作的类,如果变化可能性低,可以直接使用具体类或抽象类。
- YAGNI原则:不要为"可能"的变化提前设计接口,等到真正需要多态或替换实现时再抽取接口。