深蓝黑背景封面。左侧两张机制卡片并排:左卡片紫色描边「抽象类 / 半成品模板」,列「可以有实例字段、有构造器、方法可已实现、只能继承一个」,底部标签「复用公共实现」;右卡片青色描边「接口 / 能力契约」,列「方法签名、常量、default 方法、可实现多个」,底部标签「定义能力契约」。卡片下方横条写「一个类只能 extends 一个抽象类,但可以 implements 多个接口」,最底注「二者常搭档:接口定契约,抽象类放公共实现」。右侧大标题「抽象类和接口,到底差在哪」,副标题「能力契约,还是公共实现」。左上标注「面试经典 · Java 后端」,右上圆角胶囊标注「Day 01 · 11-08」 图 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 同时插上「可淘汰」和「可关闭」两种能力——单继承加多实现,一个不缺。

深蓝黑背景概念图。上半部是一张三列对比表:维度列依次为实例字段、构造器、继承/实现数量、方法实现、设计角色;抽象类列对应「可以有自己的状态」「有,子类构造时调用」「单继承:只能 extends 一个」「抽象方法与已实现方法任意混」「复用公共实现的半成品基类」;接口列对应「只有 public static final 常量」「没有,不能直接实例化」「可以 implements 多个」「default、static、私有方法可带实现」「定义可插拔的能力契约」。下半部左右两块决策卡:左卡青色描边「什么时候选抽象类」——多个子类共享字段与公共实现、需要构造器控制初始化流程、模板方法骨架固定步骤可覆写,例:框架基类、算子基类;右卡紫色描边「什么时候选接口」——定义能力契约且实现互不相关、需要多实现插拔(策略、回调、SPI)、要给同一个类混入多种能力,例:可检索、可序列化、可关闭 图 2:五维对比表加上两张选型决策卡。

default 方法改变了什么,又没改变什么。

接口能写 default 方法之后,「接口只能定义签名」的老说法过时了:给接口新增一个带默认实现的方法,存量实现类不用改一行代码就能平滑升级,这是契约的可演进性。但 default 方法操作不了实例状态——接口里没有字段可读写,它能复用的只是行为,复用不了数据。抽象类则相反:状态、构造流程、模板骨架都能沉淀。一句话收口:接口回答「这个类能做什么」(can-do),抽象类回答「这个类是什么、并复用什么」(is-a 加公共实现)。

设计时怎么选。

选抽象类的信号:多个子类要共享字段和公共实现、需要用构造器控制初始化流程、算法骨架固定但个别步骤留给子类覆写(模板方法)——框架基类、算子基类都是这个形态。选接口的信号:定义的是能力契约且各实现互不相关、需要多实现插拔(策略、回调、SPI 扩展点)、要给同一个类混入多种能力——可检索、可序列化、可关闭。工程里最常见的形态是两者搭档:接口定契约,抽象类实现该契约并沉淀公共逻辑,留给业务方「实现接口」或「继承基类」两种接入姿势。插件化算子、可插拔检索器这类 AI 组件设计,天天面对的就是这个选择,选错了,版本演进时都要用重构偿还。

回到面试现场,建议的答题结构:先用一句话立住「契约 vs 半成品基类」的分工,再过五维差异(重点讲字段与构造器),然后主动讲 default 方法带来的演进能力与「接口无状态」的边界,最后落到选型信号。背表是及格线,讲清「状态只能单继承、契约可以多实现」这条设计逻辑才是加分项。你在项目里有没有把本该是接口的契约写成了抽象类、后来演进时吃苦头的经历?欢迎聊聊。