这是我学习 Java 抽象机制时整理的笔记。接口(interface)和抽象类(abstract class)是面试必问的对比题,也是理解"抽象"的关键——先搞清两者各自的定位,再记区别,最后会选。
一、解决什么问题:都是"抽象"
两者都是抽象手段——只描述"是什么/能做什么",不暴露具体实现,让上层代码依赖抽象、不依赖实现(呼应《SOLID 五大设计原则》里的 D 依赖倒置)。
但它们的语义定位完全不同:
| interface | abstract class | |
|---|---|---|
| 语义 | 能做什么(can-do)——能力契约 | 是什么(is-a)——类族骨架 |
| 典型例子 | Flyable(能飞)、Runnable(能跑线程) | Animal(动物)、Shape(形状) |
二、interface:能力契约
纯契约:只声明"能做什么",默认不实现(JDK 8 后可加默认方法)。
// 能力:能飞、能说话
interface Flyable {
void fly(); // 抽象方法:实现类必须实现
}
interface Speakable {
void speak();
}
// 一个类可以实现多个接口(Java 唯一的"多继承")
class Bird implements Flyable, Speakable {
@Override public void fly() { System.out.println("鸟会飞"); }
@Override public void speak() { System.out.println("鸟会叫"); }
}关键点(全从"接口没有实例"这个本质推出——接口是能力契约,不是实物):
① 字段只能是 public static final 常量 接口没有实例,所以不能有"每个对象各不相同的值"(实例变量)——接口里的变量只能是写死的常量:
interface Shape {
int MAX_SIDES = 100; // 编译器自动补全为 public static final int MAX_SIDES = 100
}类比:接口像合同,数字是写死的条款,不是"每个签合同的人自己的数据"。
② 没有构造器(不能 new、不能有实例状态)new 一个对象要调构造器"初始化对象的数据";接口连数据都没有(只有常量)→ 没必要构造器 → 不能 new:
new Flyable(); // ❌ 编译错误:接口不能实例化
new Bird(); // ✅ 只能 new 实现类"实例状态"是什么:说白了,就是 new 出来的每个对象各自带着的那包数据。比如:
Dog a = new Dog("旺财"); // a.name = "旺财"
Dog b = new Dog("小黑"); // b.name = "小黑"name 这个值,a 和 b 各存各的(互不影响)——这种"每个对象各存各的值"就叫实例状态。
接口没有实例状态:它不能 new(没有对象),所以不存在"每个对象各存各的值",接口里的字段只能是写死的常量(所有人共享同一个值)。对比抽象类有字段、有构造器,能存实例状态(每个子类对象各存各的)。
③ JDK 8+:default 默认方法(加新方法不破坏实现类) 假设已有 100 个类实现了 Speakable,你想加个 speak()——不写 default,100 个实现类全要改(不实现就编译错误)。default 给接口方法配一个"默认实现",实现类可以不重写直接用:
interface Speakable {
default void speak() { System.out.println("默认叫声"); }
}
class Bird implements Speakable { } // 没写 speak() 也能用 → "默认叫声"意义:接口升级加方法,老实现类零改动——这就是"给已有接口加方法不破坏实现类"(第六节的验证代码里 Bird 没重写 speak() 却输出"默认叫声",就是它生效)。
④ JDK 9+:private 私有方法(接口内部抽公共逻辑) 多个 default 方法有重复逻辑怎么办?抽出来——用 private 方法:
interface Service {
default void doA() { log("A"); commonStep(); }
default void doB() { log("B"); commonStep(); }
private void commonStep() { System.out.println("公共步骤"); } // 内部工具,外部看不到
private void log(String s) { System.out.println("记录:" + s); }
}为什么必须 private:这些只是接口内部实现的辅助工具,不该暴露给实现类——private 保证"接口内部的私有逻辑"。
一句话串起来:接口是"能力契约"不是"实物"→ 所以没数据(只有常量)、不能 new(无构造器);default 是为兼容升级,private 是为内部整洁。
三、abstract class:类族骨架
半成品类:既有抽象方法(子类实现),也有具体方法和字段(子类复用)。
// 模板方法模式:骨架定好,子类填细节
abstract class Animal {
protected String name; // 有字段(状态)
Animal(String name) { this.name = name; } // 有构造器
abstract void eat(); // 抽象方法:子类必须实现
void showInfo() { // 具体方法:子类直接复用
System.out.print(name + ":");
eat();
}
}
class Dog extends Animal {
Dog() { super("狗"); }
@Override void eat() { System.out.println("吃骨头"); }
}关键点:
- 单继承(一个类只能继承一个抽象类)
- 有字段、有构造器、有具体方法——可以存状态、复用实现
- 可以有没有抽象方法的抽象类(下面展开讲)
- 经典应用:模板方法模式(骨架方法定义流程,抽象方法留给子类)
为什么会有"没有抽象方法的抽象类":abstract 的真正含义不是"必须要有抽象方法",而是"这个类不能 new,只能被继承"。有没有抽象方法是另一码事:
- 有抽象方法 → 编译器强制要求类必须是 abstract(方法没实现,new 出来没法调用);
- 没有抽象方法、但类仍然标 abstract → 唯一目的就是禁止实例化。
什么时候需要这种"纯禁止 new"的类?当一个类只是用来做分类、共享字段和方法,它本身是个笼统概念、不该被单独 new 出来。比如:
// 没有抽象方法的抽象类:纯"禁止实例化"
abstract class Fruit {
String color;
Fruit(String color) { this.color = color; }
String getColor() { return color; } // 全是具体方法,子类直接复用
}
class Apple extends Fruit {
Apple() { super("红色"); }
}
class Banana extends Fruit {
Banana() { super("黄色"); }
}现实中没人会"new 一个纯粹的水果"——你说"我要一个水果",最后拿到的不是苹果就是香蕉。Fruit 存在的意义只是给子类当爹(共享 color 字段和 getColor() 方法),所以用 abstract 告诉编译器:你不能 new 我,只能继承我。
真实案例:HttpServlet 就是这种类。后端天天写 Servlet,但注意看 JDK 源码——它是 abstract,却没有任何抽象方法:
// jakarta.servlet.http.HttpServlet(真实源码简化)
public abstract class HttpServlet extends GenericServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 默认实现:返回 405 "HTTP method GET is not supported"
}
protected void doPost(...) { /* 同样默认 405 */ }
// doPut/doDelete... 全部都有默认实现,一个 abstract 都没有
}那它为什么还要标 abstract?因为单独 new 一个 HttpServlet 没有任何意义——一个方法都没重写的 Servlet,收到任何请求都返回 405,什么都干不了。它存在的意义就是强制你继承:写 class MyServlet extends HttpServlet 后按需重写 doGet/doPost:
class MyServlet extends HttpServlet { } // ✅ 编译通过:没有抽象方法,一行不写也合法
// HttpServlet s = new HttpServlet(); // ❌ 编译错误:abstract 禁止实例化这就是"没有抽象方法的抽象类"在真实代码里的样子:类本身禁止 new,但每个方法都给了默认实现,子类按需覆盖(Java AWT 的 MouseAdapter 也是这种"适配器/钩子类")。
四、核心区别(对照表)
| 维度 | interface | abstract class |
|---|---|---|
| 语义 | 能做什么(能力) | 是什么(类族) |
| 多继承 | ✅ 一个类实现多个 | ❌ 只能继承一个 |
| 字段 | 只能 public static final 常量 | ✅ 实例字段(有状态) |
| 构造器 | ❌ 没有 | ✅ 有(子类 super 调用) |
| 方法 | 抽象 + default/static/private | 抽象 + 具体方法 |
| 实例化 | ❌ | ❌(但可以被子类 new) |
| JDK 演变 | 8 默认方法 / 9 私有方法 / 17 sealed | 一直如此 |
记忆口诀:
- 能飞能跑 → 接口(Flyable/Runnable)
- 是动物是形状 → 抽象类(Animal/Shape)
- 接口管"能力清单",抽象类管"类族骨架 + 模板流程"
五、怎么选(决策流程)
解读:
- 判断 1:只关心"能做什么",且可能被多个不相关的类实现 → interface(能力契约,可多实现)
- 例:序列化
Serializable、比较Comparable、回调Runnable
- 例:序列化
- 判断 2:描述"是什么"的类族,子类要共享字段/方法/模板流程 → abstract class(类族骨架,单继承)
- 例:支付抽象类——子类微信/支付宝共享对账逻辑
- 判断 3:契约和骨架都想要 → 抽象类实现接口(骨架 + 契约结合)
实战判断:Java 里 List/Map 都是接口(能力契约),AbstractList/AbstractMap 是抽象类(给实现类提供公共骨架)——接口定义 API,抽象类提供默认实现,这就是 JDK 大量"接口 + Abstract 抽象类"成对出现的原因。
六、实跑验证(javac 编译运行)
// 验证点:① 接口多实现 ② 抽象类单继承 ③ 模板方法 ④ 默认方法 ⑤ 无抽象方法的抽象类(禁止实例化)
interface Flyable { void fly(); }
interface Speakable { default void speak() { System.out.println("默认叫声"); } } // JDK8 默认方法
abstract class Animal {
protected String name;
Animal(String name) { this.name = name; }
abstract void eat();
void show() { System.out.print(name + ":"); eat(); } // 模板方法
}
class Bird extends Animal implements Flyable, Speakable { // 单继承 + 多实现
Bird() { super("鸟"); }
@Override public void eat() { System.out.println("吃虫"); }
@Override public void fly() { System.out.println(name + "会飞"); }
// speak() 用默认实现,不重写
}
// ⑤ 没有抽象方法的抽象类:纯"禁止实例化"(new Fruit 编译报错,只能 new 子类)
abstract class Fruit {
String color;
Fruit(String color) { this.color = color; }
String getColor() { return color; }
}
class Apple extends Fruit { Apple() { super("红色"); } }
class Banana extends Fruit { Banana() { super("黄色"); } }
public class Verify {
public static void main(String[] args) {
Bird bird = new Bird();
bird.show(); // 鸟:吃虫(模板方法:骨架调抽象方法)
bird.fly(); // 鸟会飞(接口实现)
bird.speak(); // 默认叫声(JDK8 默认方法,未重写)
Flyable f = bird; // 接口引用指向实现(多态)
f.fly();
Apple apple = new Apple();
Banana banana = new Banana();
System.out.println("苹果:" + apple.getColor()); // 子类复用父类字段/方法
System.out.println("香蕉:" + banana.getColor());
// new Fruit("红色") ❌ 编译错误:Fruit 是抽象的,不能实例化
}
}实测输出:
鸟:吃虫
鸟会飞
默认叫声
鸟会飞
苹果:红色
香蕉:黄色小结
- 接口 = 能力契约(能做什么):多实现、无状态、JDK8 默认方法
- 抽象类 = 类族骨架(是什么):单继承、有状态、模板方法模式
- 怎么选:契约/能力 → 接口;共享实现/模板 → 抽象类;
List/AbstractList是 JDK 的经典搭配 - 面向对象基础见《Java 面向对象:封装、继承、多态》(重载 vs 重写),接口的默认方法、代理等进阶见《Java 动态代理》
验证说明:接口多实现、抽象类单继承 + 模板方法、JDK8 默认方法(未重写直接生效)、无抽象方法的抽象类(子类复用字段/方法,
new Fruit编译报错)全部 javac 编译运行实测,输出与注释一致;HttpServlet案例(jakarta.servlet-api 6.0.0)同样实测——空子类编译通过 + 实例化成功,new HttpServlet()编译报错 "HttpServlet是抽象的; 无法实例化"。
