Java 中接口和抽象类的区别是什么?怎么选择?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
基础概念掌握:面试官不仅仅是想知道你会不会定义接口和抽象类,更是想知道你是否理解它们在设计层面的本质区别—— "契约" 与 "模板" 的不同定位。
-
设计能力考察:通过这个问题,面试官想评估你的面向对象设计思维,能否根据实际场景做出合理的抽象选择,而不是滥用继承或接口。
-
版本演进意识:JDK 8 之后接口引入了默认方法和静态方法,这让接口和抽象类的界限变得模糊,考察你是否跟上 Java 的演进,以及在新特性下的选择逻辑。
核心答案
一句话总结:接口定义 "能做什么"(行为契约),抽象类定义 "是什么"(本质模板)。
核心区别对比
| 对比维度 | 接口 | 抽象类 |
|---|---|---|
| 设计理念 | 行为契约,定义 "能做什么" | 模板模式,定义 "是什么" |
| 继承关系 | implements,可多实现 | extends,单继承 |
| 成员变量 | 只能是 public static final 常量 | 任意访问修饰符,可定义普通成员变量 |
| 方法实现 | JDK 8 前:只能抽象方法 JDK 8+:可有 default、static 方法JDK 9+:可有 private 方法 | 可有抽象方法和具体实现 |
| 构造方法 | ❌ 没有 | ✅ 有(供子类调用) |
| 访问修饰符 | 默认 public | 任意(public、protected、默认) |
选择口诀
- 多实现用接口,单继承用抽象;
- 行为规范用接口,代码复选用抽象;
- 跨层次用接口,同家族用抽象。
深度解析
一、从设计层面理解本质区别
接口和抽象类最大的区别在于 设计意图,用一个生活化的例子来理解:
上图展示了接口和抽象类的核心设计差异:
-
接口(
Flyable):定义的是 "飞行" 这个 能力/行为。飞机、鸟、超人都可以飞行,但它们本质完全不同,没有共同的父类。接口强调的是 "能做什么"。 -
抽象类(
Bird):定义的是 "鸟" 这个 本质/家族。麻雀、老鹰、企鹅都是鸟,它们有共同的特征(羽毛、翅膀、产卵等)和可以复用的代码。抽象类强调的是 "是什么"。
关键点:企鹅虽然不会飞(不实现 Flyable),但它依然是鸟(继承 Bird)。这就是 "本质" 和 "能力" 的区别。
二、JDK 版本演进带来的变化
上图展示了 Java 接口从 JDK 7 到 JDK 9+ 的演进过程:
-
JDK 7 及之前:接口是纯粹的抽象契约,只能包含抽象方法和常量,没有任何实现代码。
-
JDK 8:引入
default方法和static方法,允许接口提供默认实现。这是为了支持 API 演进,比如Collection接口新增forEach()、removeIf()等方法,已有的ArrayList、HashSet等实现类无需修改就能获得新功能。 -
JDK 9+:进一步引入
private方法,用于复用default方法中的公共代码逻辑,避免代码重复。
面试加分点:JDK 8 之后,接口和抽象类的界限确实变模糊了,但核心区别依然存在——接口不支持成员变量和构造方法,仍然是"轻量级"的行为规范。
三、代码示例对比
// ==================== 接口定义 ====================
public interface Flyable {
// 常量(自动 public static final)
int MAX_SPEED = 1000;
// 抽象方法(自动 public abstract)
void fly();
// JDK 8: 默认方法
default void glide() {
System.out.println("滑翔中...");
}
// JDK 8: 静态方法
static void checkWeather() {
System.out.println("检查天气状况");
}
}
// ==================== 抽象类定义 ====================
public abstract class Animal {
// 成员变量(可以有任何访问修饰符)
protected String name;
private int age;
// 构造方法
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 具体方法(子类直接继承)
public void eat() {
System.out.println(name + " 正在进食");
}
// 抽象方法(子类必须实现)
public abstract void makeSound();
// 普通方法
public int getAge() {
return age;
}
}
// ==================== 实际使用 ====================
// 飞机:实现 Flyable,继承 Object(默认)
public class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("飞机依靠引擎飞行");
}
}
// 麻雀:继承 Bird(假设 extends Animal),实现 Flyable
public class Sparrow extends Animal implements Flyable {
public Sparrow(String name, int age) {
super(name, age);
}
@Override
public void fly() {
System.out.println(name + " 扇动翅膀飞行");
}
@Override
public void makeSound() {
System.out.println("叽叽喳喳");
}
}
四、选择决策流程
上图展示了接口和抽象类的选择决策流程:
-
第一步:如果需要定义成员变量或构造方法,只能选择抽象类。这是接口无法提供的核心能力。
-
第二步:如果需要多实现(一个类同时具备多种能力),必须选择接口。Java 不支持多继承,但支持多实现。
-
第三步:如果以上条件都不满足,优先选择接口。接口更轻量、更灵活,符合"面向接口编程"的设计原则。
典型应用场景:
- 抽象类:模板方法模式(如
AbstractList)、共享代码和状态(如AbstractStringBuilder) - 接口:策略模式(如
Comparator)、回调机制(如Runnable、Callable)、标记接口(如Serializable)
五、经典应用案例对比
// ==================== 案例1: 抽象类 - 模板方法模式 ====================
// JDK 源码:java.util.AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
// 提供基础实现,子类只需实现少量方法
public E get(int index) {
// 基于 iterator() 实现
}
public int indexOf(Object o) {
// 提供通用实现
}
// 子类必须实现的核心方法
public abstract E get(int index);
}
// ArrayList 只需实现少量方法,继承大量通用逻辑
public class ArrayList<E> extends AbstractList<E> {
public E get(int index) {
// 具体实现
}
}
// ==================== 案例2: 接口 - 行为规范 ====================
// JDK 源码:Comparator 函数式接口
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// default 方法提供组合能力
default Comparator<T> reversed() {
return (o1, o2) -> compare(o2, o1);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
return (o1, o2) -> {
int res = compare(o1, o2);
return (res != 0) ? res : other.compare(o1, o2);
};
}
}
// 使用:灵活组合多种比较策略
Comparator<String> comparator = Comparator
.comparing(String::length)
.thenComparing(String::compareTo);
面试高频追问
-
JDK 8 之后接口可以有默认实现,那接口和抽象类还有什么区别?
核心区别依然存在:接口不能有成员变量(只能有常量)和构造方法,不支持状态维护;抽象类可以。接口强调"能力",抽象类强调"本质"。
-
一个类可以同时继承抽象类和实现接口吗?
可以!而且这是常见的做法。比如
ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable。 -
接口可以继承接口吗?抽象类可以实现接口吗?
接口可以多继承接口(
interface A extends B, C);抽象类可以实现接口(abstract class D implements A)。 -
什么时候用 default 方法,什么时候用抽象类?
default方法用于为已有接口添加新功能(向后兼容);如果需要共享状态或复杂的模板逻辑,仍然应该使用抽象类。
常见面试变体
- "抽象类可以没有抽象方法吗?"(可以,比如
AbstractStringBuilder) - "接口可以继承抽象类吗?"(不可以,接口只能继承接口)
- "Java 为什么不支持多继承?"(菱形继承问题、复杂性、实际需求不大)
- "什么是函数式接口?和普通接口有什么区别?"(只有一个抽象方法的接口,可用
@FunctionalInterface注解)
记忆口诀
接口定义 "能做什么"(行为能力),抽象类定义 "是什么"(本质模板);
接口可多实现,抽象类只能单继承;
接口是契约(轻量),抽象类是半成品(重量);
需要状态/构造/成员变量 → 抽象类;
需要多实现/解耦/跨层次 → 接口。
总结
接口和抽象类的核心区别在于 设计意图:接口定义 "能力"(能做什么),抽象类定义 "本质"(是什么)。选择时,优先考虑是否需要成员变量/构造方法(用抽象类),是否需要多实现(用接口)。JDK 8 之后接口可以有默认实现,但无法改变"接口无状态"的本质,遵循"面向接口编程"原则,优先使用接口,必要时才用抽象类。