图 1:本文封面,两张卡片概括两种机制的角色分工。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「抽象类和接口的区别」是 Java 面试的保留曲目,很多人会背对比表,却被一句追问问住:既然接口现在也能写方法实现,两者还有本质区别吗?有——分野不在语法,在于能不能持有状态。
语法层差异:先过一张五维清单。
抽象类是「半成品基类」:可以有自己的实例字段(状态)、有构造器(不能直接实例化,但子类构造时会调用)、抽象方法与已实现方法任意混合、一个类只能 extends 一个抽象类。接口是「能力契约」:没有实例字段——Java 语言规范第 9 章明确接口体中每个字段声明都隐含 public static final,写出来的是常量不是状态;没有构造器;一个类可以 implements 任意多个接口;方法可以有抽象方法,也可以有带实现的 default、static 和 private 方法。
这五条里,前两条(字段与构造器)是真正的分水岭,后三条是推论。
编译器亲口告诉你边界。
别只背表,让编译器说话。写两个故意越界的声明:
abstract class A { }
abstract class B { }
interface Counter { int count; } // 接口字段必须是常量
class C extends A, B { } // 类只能单继承
用 JDK 21 编译(javac Bad.java),两个报错如约而至:
Bad.java:3: error: = expected
interface Counter { int count; }
^
Bad.java:4: error: '{' expected
class C extends A, B { }
^
2 errors
第一处:接口里 int count; 没有初始化器直接编不过——接口字段必须当场赋值,因为它本质上是常量。第二处:extends A, B 语法层面就不存在,Java 类是单继承。这两条红线合起来说明一件事:状态与构造的继承只能有一个来源,行为契约可以有多个来源——这正是语言设计者刻意留下的分工。
一段代码看两种机制怎么搭档。
下面这个迷你缓存例子让抽象类提供公共状态与模板骨架,让接口提供可插拔能力。用 JDK 21 编译运行(javac AbsVsIface.java && java AbsVsIface):
abstract class AbstractCache {
protected final String name; // 抽象类可以有实例字段
protected int hitCount = 0;
AbstractCache(String name) { // 抽象类可以有构造器
this.name = name;
System.out.println("AbstractCache 构造器执行,name=" + name);
}
abstract Object load(String key); // 留给子类的抽象方法
final Object get(String key) { // 模板方法:骨架固定,步骤可覆写
Object v = load(key);
if (v != null) hitCount++;
return v;
}
}
interface Evictable {
int MAX_ENTRIES = 1024; // 隐含 public static final
void evict();
default String evictPolicy() { // default 方法携带默认实现
return policyName() + ",容量上限 " + MAX_ENTRIES;
}
private String policyName() { // 接口私有方法,只服务 default 方法
return "LRU";
}
static Evictable noOp() { // 接口静态方法
return () -> System.out.println("no-op evict");
}
}
class MemoryCache extends AbstractCache implements Evictable, AutoCloseable {
MemoryCache() { super("memory"); }
@Override Object load(String key) { return "value-of-" + key; }
@Override public void evict() { System.out.println("按 " + evictPolicy() + " 淘汰"); }
@Override public void close() { System.out.println("资源已关闭"); }
}
public class AbsVsIface {
public static void main(String[] args) {
MemoryCache c = new MemoryCache();
c.get("k1"); c.get("k2");
System.out.println("命中次数 hitCount = " + c.hitCount);
c.evict();
Evictable.noOp().evict();
c.close();
}
}
实测输出:
AbstractCache 构造器执行,name=memory
命中次数 hitCount = 2
按 LRU,容量上限 1024 淘汰
no-op evict
资源已关闭
输出里能看到整条协作链:子类构造时先跑抽象类构造器完成状态初始化;hitCount 这种状态住在抽象类里,接口给不了;Evictable 用 default 方法携带默认行为、用 private 方法隐藏内部细节,MemoryCache 同时插上「可淘汰」和「可关闭」两种能力——单继承加多实现,一个不缺。
图 2:五维对比表加上两张选型决策卡。
default 方法改变了什么,又没改变什么。
接口能写 default 方法之后,「接口只能定义签名」的老说法过时了:给接口新增一个带默认实现的方法,存量实现类不用改一行代码就能平滑升级,这是契约的可演进性。但 default 方法操作不了实例状态——接口里没有字段可读写,它能复用的只是行为,复用不了数据。抽象类则相反:状态、构造流程、模板骨架都能沉淀。一句话收口:接口回答「这个类能做什么」(can-do),抽象类回答「这个类是什么、并复用什么」(is-a 加公共实现)。
设计时怎么选。
选抽象类的信号:多个子类要共享字段和公共实现、需要用构造器控制初始化流程、算法骨架固定但个别步骤留给子类覆写(模板方法)——框架基类、算子基类都是这个形态。选接口的信号:定义的是能力契约且各实现互不相关、需要多实现插拔(策略、回调、SPI 扩展点)、要给同一个类混入多种能力——可检索、可序列化、可关闭。工程里最常见的形态是两者搭档:接口定契约,抽象类实现该契约并沉淀公共逻辑,留给业务方「实现接口」或「继承基类」两种接入姿势。插件化算子、可插拔检索器这类 AI 组件设计,天天面对的就是这个选择,选错了,版本演进时都要用重构偿还。
回到面试现场,建议的答题结构:先用一句话立住「契约 vs 半成品基类」的分工,再过五维差异(重点讲字段与构造器),然后主动讲 default 方法带来的演进能力与「接口无状态」的边界,最后落到选型信号。背表是及格线,讲清「状态只能单继承、契约可以多实现」这条设计逻辑才是加分项。你在项目里有没有把本该是接口的契约写成了抽象类、后来演进时吃苦头的经历?欢迎聊聊。