抽象类与接口
核心概念
抽象类(Abstract Class)和接口(Interface)都是Java中定义抽象层次的方式,但设计思想和使用场景不同。理解它们的区别和演进,是设计良好架构的基础。
一、抽象类(Abstract Class)
1.1 基本语法
public abstract class Animal {
// 普通字段
protected String name;
// 构造函数(不能直接实例化,但子类可以调用)
public Animal(String name) {
this.name = name;
}
// 普通方法
public void sleep() {
System.out.println(name + " is sleeping");
}
// 抽象方法(没有方法体,子类必须实现)
public abstract void speak();
}
// 子类必须实现所有抽象方法
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(name + " says: Woof!");
}
}
1.2 抽象类的特点
| 特点 | 说明 |
|---|---|
| 不能实例化 | new Animal() 编译错误 |
| 可以有构造函数 | 供子类调用 |
| 可以有普通方法 | 提供默认实现 |
| 可以有抽象方法 | 子类必须实现 |
| 可以有字段 | 任意访问修饰符 |
| 单继承 | 只能extends一个类 |
1.3 抽象类的设计意图
// 模板方法模式:定义算法骨架,子类实现具体步骤
public abstract class AbstractOrderProcessor {
// 模板方法(final防止子类修改流程)
public final void processOrder(Order order) {
validateOrder(order);
calculatePrice(order);
applyDiscount(order); // 抽象方法,子类实现
saveOrder(order);
sendNotification(order); // 钩子方法,子类可选重写
}
private void validateOrder(Order order) { /* 通用验证 */ }
private void calculatePrice(Order order) { /* 通用计算 */ }
private void saveOrder(Order order) { /* 通用保存 */ }
// 抽象方法:子类必须实现
protected abstract void applyDiscount(Order order);
// 钩子方法:子类可选重写
protected void sendNotification(Order order) {
// 默认实现:发送邮件
}
}
// 具体实现
public class VIPOrderProcessor extends AbstractOrderProcessor {
@Override
protected void applyDiscount(Order order) {
order.setDiscount(0.8); // VIP 8折
}
@Override
protected void sendNotification(Order order) {
// VIP专属通知
}
}
二、接口(Interface)
2.1 JDK 7及之前
public interface Flyable {
// 常量(默认 public static final)
int MAX_SPEED = 1000;
// 抽象方法(默认 public abstract)
void fly();
void land();
}
// 实现类
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("Bird is flying");
}
@Override
public void land() {
System.out.println("Bird is landing");
}
}
2.2 JDK 8接口增强
public interface Flyable {
// 抽象方法
void fly();
// 默认方法(default):提供默认实现,实现类可选重写
default void takeOff() {
System.out.println("Taking off...");
}
// 静态方法(static):通过接口名调用
static void showInfo() {
System.out.println("This is Flyable interface");
}
}
// 使用
Bird bird = new Bird();
bird.fly(); // 实现类方法
bird.takeOff(); // 默认方法
Flyable.showInfo(); // 静态方法
2.3 JDK 9接口增强
public interface Flyable {
void fly();
default void takeOff() {
prepare(); // 调用私有方法
System.out.println("Taking off...");
}
default void land() {
System.out.println("Landing...");
cleanup(); // 调用私有方法
}
// 私有方法(private):提取公共逻辑,仅接口内部使用
private void prepare() {
System.out.println("Preparing...");
}
private void cleanup() {
System.out.println("Cleaning up...");
}
// 私有静态方法
private static void log(String msg) {
System.out.println("[LOG] " + msg);
}
}
2.4 接口演进总结
| 版本 | 新增特性 | 说明 |
|---|---|---|
| JDK 7 | 常量、抽象方法 | 纯抽象 |
| JDK 8 | default方法、static方法 | 可以有实现 |
| JDK 9 | private方法、private static方法 | 代码复用 |
三、抽象类 vs 接口
3.1 核心区别
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 继承 | 单继承 | 多实现 |
| 构造函数 | 可以有 | 不能有 |
| 字段 | 任意类型 | 只能public static final |
| 方法 | 任意类型 | public(JDK 9+可private) |
| 设计思想 | is-a(是什么) | can-do(能做什么) |
| 演进成本 | 低(可加普通方法) | 高(JDK 8前加方法会破坏实现类) |
3.2 设计思想对比
接口更像是一种行为规范,强调“能做什么”;抽象类更像是一种模板,强调“应该怎么做”。
从设计思路上来说:接口通常是自上而下的设计,我们先定义好一个行为契约(比如 Runnable),然后不同的类根据自己的能力去实现它。这种方式适合“能力横切”的场景(像排序、序列化),这些类之间未必有继承关系。这种设计强调先约定行为,再去实现,面向接口编程就是这个思想的体现。
而抽象类的设计更偏向于自下而上,我们往往是写了一批类之后,发现它们之间有一些通用逻辑或状态,于是把这些共性抽象出来,形成一个抽象父类,减少重复代码。比如所有动物都有名字、有吃饭的方法,那就可以抽出一个 Animal 抽象类。
另外一个比较关键区别在于:Java 支持 接口的多实现**,不支持抽象类的多继承。**
这是因为 Java 要避免多继承带来的复杂性,特别是菱形继承问题,这种问题很容易引起成员冲突和继承歧义,所以一个类只能继承一个抽象类。但接口就没这种问题,接口里没有普通的字段状态,它的变量都是 static final 的,也不依赖构造方法,天生就是用来声明行为的,所以可以允许多实现,不会有状态冲突,一个类可以实现多个接口。(没有状态、不依赖构造器,自然避免多继承冲突)
从语法角度补充几点:
- 接口只能用 implements 关键字来实现,抽象类只能用 extends 关键字来单继承。
- 接口里的方法默认是 public 的,字段是 public static final 的。
- 抽象类可以有各种访问修饰符、普通字段和构造器。
- 抽象类里可以写通用逻辑,接口主要声明行为规范,在 Java 8+ 得到了增强(default、static、private 方法),在保留行为约定特性的同时也具备了部分逻辑复用能力,但依然不能有构造函数,不能持有实例状态,这也是它与抽象类的根本区别。
在选择上:
- 如果是多个无关类要统一某个能力,优先接口(如 Comparable、Serializable)。
- 如果是多个相关类共享部分代码逻辑,适合抽象类。
一句话总结:接口强调“行为契约”,适合多实现;抽象类强调“逻辑复用”,只适合单继承。
// 抽象类:is-a 关系(是什么)
abstract class Animal {
protected String name;
public abstract void speak();
}
class Dog extends Animal {
// Dog is an Animal
}
// 接口:can-do 关系(能做什么)
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Duck extends Animal implements Flyable, Swimmable {
// Duck is an Animal
// Duck can fly
// Duck can swim
}
3.3 选择原则
需要定义一组行为规范?
├── 是 → 接口
└── 否 → 需要共享代码或状态?
├── 是 → 抽象类
└── 否 → 需要多继承?
├── 是 → 接口
└── 否 → 根据语义选择
选择接口的场景:
- 定义能力/行为(Comparable、Serializable)
- 需要多继承
- 不相关的类需要实现相同行为
选择抽象类的场景:
- 有共享的代码或状态
- 需要非public成员
- 需要构造函数
- 紧密相关的类之间共享实现
3.4 组合使用
// 接口定义能力
public interface Drawable {
void draw();
}
// 抽象类提供部分实现
public abstract class Shape implements Drawable {
protected String color;
public Shape(String color) {
this.color = color;
}
public String getColor() {
return color;
}
// 留给子类实现
@Override
public abstract void draw();
}
// 具体类
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing " + color + " circle with radius " + radius);
}
}
四、接口的默认方法冲突
4.1 类优先原则
interface A {
default void method() {
System.out.println("A");
}
}
class B {
public void method() {
System.out.println("B");
}
}
class C extends B implements A {
// 不需要重写,类的方法优先
}
new C().method(); // 输出:B
4.2 接口冲突
interface A {
default void method() {
System.out.println("A");
}
}
interface B {
default void method() {
System.out.println("B");
}
}
// 必须重写解决冲突
class C implements A, B {
@Override
public void method() {
A.super.method(); // 选择调用A的默认方法
// 或 B.super.method();
// 或自己实现
}
}
4.3 冲突解决规则
- 类优先:类中的方法优先于接口默认方法
- 子接口优先:更具体的接口优先
- 显式选择:如果仍有冲突,必须重写并显式选择
五、函数式接口
5.1 定义
// 只有一个抽象方法的接口
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// 可以有默认方法
default Comparator<T> reversed() {
return (o1, o2) -> compare(o2, o1);
}
// 可以有静态方法
static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (o1, o2) -> o1.compareTo(o2);
}
}
5.2 常用函数式接口
| 接口 | 方法 | 用途 |
|---|---|---|
Runnable |
void run() |
无参无返回 |
Supplier<T> |
T get() |
无参有返回 |
Consumer<T> |
void accept(T t) |
有参无返回 |
Function<T,R> |
R apply(T t) |
有参有返回 |
Predicate<T> |
boolean test(T t) |
判断 |
5.3 Lambda表达式
// 传统写法
Comparator<String> c1 = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
};
// Lambda写法
Comparator<String> c2 = (o1, o2) -> o1.length() - o2.length();
// 方法引用
Comparator<String> c3 = Comparator.comparingInt(String::length);
六、高频面试题
Q1: 抽象类和接口有什么区别?
答:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 继承 | 单继承 | 多实现 |
| 构造函数 | 有 | 无 |
| 字段 | 任意 | public static final |
| 设计思想 | is-a | can-do |
Q2: JDK 8对接口做了哪些增强?
答:
- default方法:提供默认实现,解决接口演进问题
- static方法:工具方法,通过接口名调用
- 函数式接口:支持Lambda表达式
Q3: 什么时候用抽象类,什么时候用接口?
答:
- 使用接口:
- 定义行为规范
- 需要多继承
- 不相关的类需要共享行为
- 轻量级的行为定义
- 使用抽象类:
- 需要共享代码和状态
- 有共同的基础实现
- 需要非public成员
- 需要构造函数
- 紧密相关的类
Q4: 接口的默认方法解决了什么问题?
答:
- 演进问题:在不破坏现有实现的情况下,为接口添加新方法
- 代码复用:避免在所有实现类中重复编写相同逻辑
- 减少抽象类使用:提供类似抽象类的代码共享能力
Q5: 为什么Java不支持多继承?
答:
- 菱形继承问题:多个父类有同名方法,子类无法确定调用哪个
- 复杂性:增加语言复杂度和编译器实现难度
- 替代方案:接口 + 组合模式
Q6: 函数式接口的作用是什么?
答:
- 只有一个抽象方法的接口
- 支持Lambda表达式和方法引用
- 简化匿名内部类的写法
- 函数式编程的基础
Q7: 接口的静态方法和默认方法有什么区别?
答:
- 静态方法:
- 通过接口名调用
- 不能被实现类重写
- 属于接口本身
- 默认方法:
- 实例方法
- 可以被实现类重写
- 提供默认实现
七、最佳实践
✅ 推荐做法
// 1. 接口定义清晰的行为规范
public interface Playable {
void play();
// 提供通用实现
default void pause() {
System.out.println("Pausing...");
}
}
// 2. 抽象类提供通用实现
public abstract class AbstractPlayer implements Playable {
protected String name;
public AbstractPlayer(String name) {
this.name = name;
}
// 部分实现
public void displayInfo() {
System.out.println("Player: " + name);
}
}
// 3. 组合优于继承
public class MusicPlayer {
private Playable device; // 组合
public void setDevice(Playable device) {
this.device = device;
}
}
// 4. 使用函数式接口简化代码
public interface Validator<T> {
boolean validate(T t);
// 组合验证
default Validator<T> and(Validator<T> other) {
return t -> this.validate(t) && other.validate(t);
}
}
// Lambda表达式
Validator<String> notEmpty = s -> !s.isEmpty();
Validator<String> minLength = s -> s.length() >= 6;
Validator<String> passwordValidator = notEmpty.and(minLength);
❌ 避免做法
// 1. 过度使用接口
public interface Everything {
void doSomething();
void doSomethingElse();
// 接口膨胀,职责不清
}
// 2. 滥用默认方法
public interface DatabaseOperation {
default void connect() {
// 太多细节实现
}
default void disconnect() {
// 接口不应该包含太多实现
}
}
// 3. 忽视接口冲突
public class BadImplementation implements A, B {
// 没有解决方法冲突
}
八、设计模式应用
8.1 策略模式
// 定义策略接口
public interface PaymentStrategy {
void pay(int amount);
}
// 具体策略实现
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paying " + amount + " by credit card");
}
}
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paying " + amount + " by PayPal");
}
}
// 上下文类
public class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void executePayment(int amount) {
strategy.pay(amount);
}
}
// 使用
PaymentContext context = new PaymentContext();
context.setStrategy(new CreditCardPayment());
context.executePayment(100);
8.2 模板方法模式
// 抽象类定义算法骨架
public abstract class DataProcessor {
// 模板方法
public final void process() {
readData();
processData();
saveData();
}
// 公共方法
private void readData() {
System.out.println("Reading data...");
}
private void saveData() {
System.out.println("Saving data...");
}
// 抽象方法,子类实现
protected abstract void processData();
}
// 具体实现
public class CSVProcessor extends DataProcessor {
@Override
protected void processData() {
System.out.println("Processing CSV data");
}
}
public class JSONProcessor extends DataProcessor {
@Override
protected void processData() {
System.out.println("Processing JSON data");
}
}
相关链接
记忆口诀
抽象类可有状态,接口定义能力
默认方法解演进,静态方法类级别
单继承多实现,is-a和can-do分明
组合优于继承,设计模式显神明
相关笔记
本篇是主战场(语法+设计取舍);抽象类与接口的区别 做了逐条对比速记,抽象类与接口的结合 补了模板方法模式与选型标准,可以当本篇的延伸阅读。
💬 评论