1. 抽象类:回顾基础
在开始比较之前,先回顾一下什么是抽象类。
抽象类——是一种不能被直接实例化的类(不能写 new Animal()),但它既可以包含普通方法(有实现),也可以包含抽象方法(无实现)。抽象类常被用作其他类的基础,这些子类继承其行为并且/或者必须实现某些方法。
例如,在我们的应用中如果有不同的交通工具种类,可以定义一个抽象类 Transport:
public abstract class Transport {
private String model;
public Transport(String model) {
this.model = model;
}
public String getModel() {
return model;
}
// 抽象方法——没有实现,只有声明
public abstract void move();
// 普通方法——有实现
public void printInfo() {
System.out.println("交通工具型号:" + model);
}
}
抽象类的特点:
- 可以包含字段(状态)。
- 可以包含已实现的方法。
- 可以包含抽象方法(子类必须实现)。
- 不能直接创建实例。
- 用于抽取通用的行为与状态。
2. 接口:回顾基础
接口是一组类必须实现的方法。接口不描述状态(不能有普通字段,只能有常量),并且在 Java 8 之前不包含方法实现。接口是一个纯契约:“如果你实现了我,你就必须会做这些事”。
接口示例:
public interface Movable {
void move(int x, int y);
}
接口的特点:
- 不包含状态(只有 public static final 常量)。
- 在 Java 8 之前——只有抽象方法(自 Java 8 起出现了 default 和 static 方法,稍后再谈)。
- 方法默认总是 public abstract。
- 一个类可以实现多个接口。
- 用于描述能力,即类“能做什么”。
3. 对比表:抽象类 vs 接口
是时候把这两种工具放在一起比较了!下面是一张直观的表格:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 语法 | |
|
| 能创建实例吗? | 不能 | 不能 |
| 能包含普通方法吗? | 可以 | Java 8 之前——不能,自 Java 8 起——只能有 default/static |
| 能包含抽象方法吗? | 可以 | 可以(Java 8 之前所有方法都是抽象的) |
| 能包含字段(状态)吗? | 可以(任意字段) | 仅 public static final(常量) |
| 能包含构造器吗? | 可以 | 不能 |
| 继承 | 只能继承一个抽象/普通类 | 可以实现多个接口 |
| 方法修饰符 | 任意(public、protected、private) | 方法默认是 public abstract。自 Java 9 起,可以添加 private 方法供接口内部使用 |
| 通过关键字 | |
|
| 常见用途 | 共享实现与状态 | 描述能力与角色 |
| JDK 中的示例 | |
|
4. 何时使用接口,何时使用抽象类?
在以下情况下使用接口:
- 你只想描述类“能做什么”,而不关心它如何实现。
- 你需要让一个类能够实现多个彼此独立的能力。
- 示例:Comparable(可以比较)、Serializable(可以序列化)、Runnable(可以在线程中运行)。
在以下情况下使用抽象类:
- 你想为所有子类提供统一的实现和状态。
- 你需要所有子类都拥有特定的字段或已实现的方法。
- 继承是严格的“单继承”:一个类只能继承一个(普通或抽象)类。
生活类比
- 接口就像“驾驶证”:如果你拥有它,你就可以驾驶汽车,但没人规定你具体开什么车、怎么开。
- 抽象类就像“通用的汽车蓝图”:所有车都有方向盘、踏板、发动机,但每个品牌会以各自的方式实现细节。
5. Java 标准库中的示例
接口:Comparable
public interface Comparable<T> {
int compareTo(T o);
}
任何实现该接口的类都必须实现方法 compareTo。例如,String、Integer、LocalDate 等等。
抽象类:AbstractList
public abstract class AbstractList<E> implements List<E> {
// 默认实现了部分 List 方法
// 有些方法保持抽象
}
AbstractList已经实现了集合的一部分行为(例如添加/删除方法),但保留了一些方法为抽象,以便子类可以按照自己的方式实现。
6. 代码示例:实践中的对比
接口
创建一个接口及其实现类。
public interface Printable {
void print();
}
public class Document implements Printable {
@Override
public void print() {
System.out.println("正在打印文档…");
}
}
抽象类
接下来是一个抽象类及其子类。
public abstract class Machine {
public void turnOn() {
System.out.println("机器已启动。");
}
public abstract void work();
}
public class Printer extends Machine {
@Override
public void work() {
System.out.println("打印机正在打印…");
}
}
类同时继承抽象类并实现接口
public class SmartPrinter extends Machine implements Printable {
@Override
public void work() {
System.out.println("智能打印机正在工作…");
}
@Override
public void print() {
System.out.println("智能打印机正在打印…");
}
}
7. 同时实现多个接口:为什么很强大
在 Java 中,一个类只能继承一个类(抽象或普通),但可以实现任意多个接口!这使你能够创建灵活、可扩展的架构。
public interface Scannable {
void scan();
}
public class MultiFunctionPrinter extends Machine implements Printable, Scannable {
@Override
public void work() {
System.out.println("多功能一体机正在工作…");
}
@Override
public void print() {
System.out.println("多功能一体机正在打印…");
}
@Override
public void scan() {
System.out.println("多功能一体机正在扫描…");
}
}
什么时候该选哪一个?
- 如果你在设计带有共享状态的基础功能(例如字段),请使用抽象类。
- 如果你想为对象添加“能力标签”(例如“能打印”“能比较”“能序列化”)——请使用接口。
- 如果不确定——从接口开始。在 Java 中这被视为良好实践:接口带来更高的灵活性与可扩展性。
8. 常见错误与坑点
错误 1:尝试继承多个类——Java 不允许!
一个类只能继承一个类,但可以实现多个接口。比如,class A extends B, C——这是错误的;而 class A extends B implements X, Y, Z——这是允许的。
错误 2:混淆了接口的字段与类的字段。
在接口中只能声明常量(public static final)。不能声明普通状态,例如 private int count;——编译器会立即阻止你。
错误 3:没有实现接口的全部方法。
如果类没有实现接口中的任意一个方法——它就必须被声明为 abstract,否则编译器会报错。
错误 4:试图创建接口或抽象类的实例.
这两种类型都是“半成品”。它们只能被扩展,不能直接实例化:
Printable p = new Printable(); // 错误!
Machine m = new Machine(); // 错误!
错误 5:认为接口可以有构造器。
接口不能有构造器,因为它们不描述对象的状态。只有类(普通类与抽象类)才可以。
GO TO FULL VERSION