摘要: 本文深入解析Java接口(Interface)的核心概念与应用,从接口作为“纯契约”的本质出发,详细讲解其定义、实现、多实现特性,以及Java 8引入的默认方法、静态方法和函数式接口等现代特性。通过对比接口与抽象类的差异,并结合JDK 21的密封接口等新特性,帮助开发者理解如何运用接口实现代码解耦、增强灵活性与可扩展性。
关键词: Java接口, 默认方法, 函数式接口, 多实现, 密封接口
我们在上一篇学习了抽象类。它是“半成品”的父类,既可以提供公共的实现,也可以定义必须由子类实现的抽象方法。它表达的是 “is-a” 关系——狗是一种动物,学生是一个人。
但现实中,除了“是什么”,我们还会关心 “能做什么” 。比如:
- 一只狗能 叫,一只猫也能 叫,但它们不是同一种动物,也不一定继承自同一个父类。
- 一个 PDF 文档能 打印,一张图片也能 打印,但它们之间没有任何继承关系。
- 一个智能设备能 充电,一部手机能 充电,一块手表也能 充电——它们可能分属完全不同的类别。
这些“能做什么”的能力,在 Java 中用什么来表达呢?
答案是 接口(Interface)。
如果说抽象类是“不完全的蓝图”,那接口就是 “一份纯粹的契约”——它只规定“你必须能做什么”,但不规定“你怎么做”。任何类,不管它继承自谁,只要实现了这个接口,就表示它承诺了这份契约。
1. 接口是什么?—— 一份“纯契约”
接口(Interface) 是一种引用类型,它定义了一组抽象方法(以及 Java 8 后的默认方法和静态方法),但不包含任何实现。
接口的核心作用是:
- 定义规范:规定一个类应该具备哪些行为。
- 解耦:调用者只依赖接口,不依赖具体实现,从而降低耦合度。
- 实现多继承:一个类可以实现多个接口,弥补 Java 单继承的不足。
打个比方:你去餐厅吃饭,你只关心“这家餐厅能不能点餐”——这就是一个接口。至于它怎么做的、用的是哪家供应商、厨师是谁,你不需要知道。任何餐厅只要实现了“可点餐”这个接口,你就能进去吃饭。接口把“你需要什么”和“谁提供给你”分开了。
2. 接口的定义与实现
Java 中使用 interface 关键字定义接口,使用 implements 关键字让一个类实现接口。
① 定义接口
public interface Flyable {
// 接口中的所有方法默认是 public abstract 的
void fly();
// 也可以显式写出来,但通常不写
public abstract void land();
}
接口中的所有方法默认都是 public abstract,所以你可以省略这些修饰符。
② 实现接口
一个类通过 implements 实现接口,并且必须实现接口中所有抽象方法(除非这个类也是抽象的)。
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("小鸟在天空中飞翔");
}
@Override
public void land() {
System.out.println("小鸟降落在树枝上");
}
}
public class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("飞机在巡航中");
}
@Override
public void land() {
System.out.println("飞机降落在跑道上");
}
}
现在,你可以用 Flyable 类型引用 Bird 和 Airplane 对象,享受多态:
Flyable f1 = new Bird();
Flyable f2 = new Airplane();
f1.fly(); // 小鸟在天空中飞翔
f2.fly(); // 飞机在巡航中
注意到了吗?Bird 和 Airplane 没有任何继承关系,但它们都实现了 Flyable,所以可以被统一处理。这比继承更加灵活。
3. 接口的多实现 —— 一个类可以“签约”多个接口
一个类可以实现多个接口,用逗号分隔。这相当于一个类同时承诺了多份契约。
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
public interface Runnable {
void run();
}
// Duck 能飞、能游泳、能跑
public class Duck implements Flyable, Swimmable, Runnable {
@Override
public void fly() {
System.out.println("鸭子飞了一小段");
}
@Override
public void swim() {
System.out.println("鸭子在水中游泳");
}
@Override
public void run() {
System.out.println("鸭子摇摇摆摆地跑");
}
}
这弥补了 Java 单继承的局限。一个 Duck 只能继承自一个父类(比如 Animal),但它可以拥有飞行、游泳、跑步三种能力。
4. 接口中的默认方法(default method)—— Java 8 的“升级”
在 Java 8 之前,接口只能包含抽象方法。这意味着一旦你往接口里增加一个新方法,所有实现类都必须修改,这对框架设计者来说是非常痛苦的。
从 Java 8 开始,接口允许包含 默认方法(default method)。默认方法有方法体,实现类可以选择继承它(不重写),也可以选择重写它。
public interface Loggable {
// 抽象方法:实现类必须实现
String getLogMessage();
// 默认方法:实现类可以直接用,也可以重写
default void log() {
System.out.println("[LOG] " + getLogMessage());
}
// 默认方法还可以有更复杂的实现
default void logWithTimestamp() {
System.out.println("[LOG at " + System.currentTimeMillis() + "] " + getLogMessage());
}
}
使用默认方法的好处是:当你给接口增加新方法时,不会破坏已有的实现类。
⚠️ 默认方法的冲突问题:如果一个类实现了两个接口,这两个接口有相同的默认方法签名,就会产生冲突。这时实现类必须重写该方法,解决冲突。
public interface A {
default void hello() {
System.out.println("Hello from A");
}
}
public interface B {
default void hello() {
System.out.println("Hello from B");
}
}
public class MyClass implements A, B {
// 必须重写,解决冲突
@Override
public void hello() {
// 可以指定调用哪个接口的默认实现
A.super.hello();
// 或者自己实现
System.out.println("Hello from MyClass");
}
}
5. 接口中的静态方法 —— Java 8 的另一项增强
从 Java 8 开始,接口还可以定义 静态方法(static method)。它们属于接口本身,通过 接口名.方法名() 调用,不通过实例。
public interface MathUtils {
// 静态方法:工具方法
static int add(int a, int b) {
return a + b;
}
static int multiply(int a, int b) {
return a * b;
}
}
// 调用方式
int sum = MathUtils.add(5, 3); // 8
int product = MathUtils.multiply(5, 3); // 15
接口中的静态方法通常用于提供与接口相关的工具方法,和普通类的静态方法类似。
6. 函数式接口 —— 一种特殊的接口
如果一个接口只包含一个抽象方法(可以有多个默认方法或静态方法),它就被称为 函数式接口(Functional Interface)。
Java 8 引入这个概念的背景是 Lambda 表达式——函数式接口允许你用更简洁的方式传递行为。我们会在第四阶段专门讲 Lambda 和 Stream,现在你只需要知道这个概念。
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
// 只能有一个抽象方法,否则 @FunctionalInterface 会报错
}
@FunctionalInterface 是一个注解(标记),它告诉编译器:“这是一个函数式接口,请帮我检查它是否只有一个抽象方法。”虽然不是必须的,但加上它是一个好习惯。
Java 标准库中常见的函数式接口有:
Runnable:无参数、无返回值Callable<V>:无参数、有返回值Comparator<T>:比较两个对象Predicate<T>:条件判断(返回 boolean)Function<T, R>:类型转换
我们以后会经常用到它们。
7. 接口与抽象类的对比
它们都是抽象机制,但各有侧重,我们用一个表格来对比:
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class | interface |
| 实例化 | ❌ 不能 | ❌ 不能 |
| 构造方法 | ✅ 可以有(供子类调用) | ❌ 不能有 |
| 成员变量 | ✅ 可以有 | ❌ 只能是 public static final(常量) |
| 普通方法 | ✅ 可以有 | ✅ 可以有(默认方法、静态方法) |
| 抽象方法 | ✅ 可以有 | ✅ 可以有(且默认就是 public abstract) |
| 多继承 | ❌ 单继承 | ✅ 一个类可以实现多个接口 |
| 表达关系 | “is-a” 关系(是什么) | “can-do” 关系(能做什么) |
| 什么时候用 | 高度相关的一组类,需要共享代码 | 不相关但具有相同行为的类,需要定义契约 |
选择指南:
- 如果多个类之间确实存在“is-a”关系,且有大量公共代码可以复用,用抽象类。
- 如果只是需要“多个不相关的类都能做某件事”,用接口。
- 在 Java 中,优先使用接口定义类型,因为接口更灵活(支持多实现),更轻量(没有状态)。
实际开发中,最佳实践是:定义接口来声明能力,用抽象类来提供部分公共实现,最后用具体类完成全部实现。这种三层结构非常常见。
8. JDK 21 接口的新特性 —— 密封接口(Sealed Interfaces)
JDK 17 引入了密封类(Sealed Classes),JDK 21 延续了对密封接口的支持。
密封接口允许你控制哪些类可以实现这个接口,这在构建领域模型或框架时非常有用。
// 只允许 Dog、Cat、Bird 实现 Animal
public sealed interface Animal permits Dog, Cat, Bird {
void sound();
}
public final class Dog implements Animal {
@Override
public void sound() {
System.out.println("汪汪");
}
}
public final class Cat implements Animal {
@Override
public void sound() {
System.out.println("喵喵");
}
}
// 如果其他类试图实现 Animal,编译会报错
密封接口的意义:你向外部提供了一组受控的扩展点,保证了类型的封闭性。这在模式匹配中尤其重要——因为你知道所有可能的子类型,可以做到穷尽性检查。
9. 综合示例 —— 一个完整的接口应用
让我们综合一下今天的内容,写一个更完整的例子:
// ========== 定义接口 ==========
/**
* 可充电接口
*/
public interface Chargeable {
void charge(); // 充电
// 默认方法:检查电量
default void checkBattery() {
System.out.println("电池状态正常");
}
}
/**
* 可联网接口
*/
public interface Networkable {
void connect(); // 联网
}
/**
* 可存储接口
*/
public interface Storable {
void save(String data);
String load();
}
// ========== 实现类:智能手表 ==========
public class SmartWatch implements Chargeable, Networkable {
private String model;
public SmartWatch(String model) {
this.model = model;
}
@Override
public void charge() {
System.out.println(model + " 正在无线充电");
}
@Override
public void connect() {
System.out.println(model + " 通过蓝牙连接手机");
}
// 可以重写默认方法
@Override
public void checkBattery() {
System.out.println(model + " 电池还剩 80%");
}
public void showTime() {
System.out.println("当前时间:10:30");
}
}
// ========== 实现类:智能音箱 ==========
public class SmartSpeaker implements Chargeable, Networkable, Storable {
private String name;
private String memory = "";
public SmartSpeaker(String name) {
this.name = name;
}
@Override
public void charge() {
System.out.println(name + " 正在通过 USB-C 充电");
}
@Override
public void connect() {
System.out.println(name + " 通过 Wi-Fi 连接互联网");
}
@Override
public void save(String data) {
memory = data;
System.out.println(name + " 已保存:" + data);
}
@Override
public String load() {
return memory;
}
public void playMusic() {
System.out.println(name + " 正在播放音乐");
}
}
// ========== 主程序 ==========
public class Main {
public static void main(String[] args) {
// 1. 多接口多态
SmartWatch watch = new SmartWatch("Apple Watch");
SmartSpeaker speaker = new SmartSpeaker("小爱同学");
// 统一按 Chargeable 接口处理
Chargeable[] devices = {watch, speaker};
for (Chargeable d : devices) {
d.charge();
d.checkBattery(); // 接口的默认方法
System.out.println("---");
}
// 2. 按 Networkable 接口处理
Networkable[] networkDevices = {watch, speaker};
for (Networkable n : networkDevices) {
n.connect();
}
// 3. 按 Storable 接口处理
speaker.save("今天的天气真好");
String data = speaker.load();
System.out.println("读取的数据:" + data);
}
}
这个例子展示了:
- 一个类可以实现多个接口(
SmartWatch实现了Chargeable和Networkable) - 多个不相关的类可以实现同一个接口(
SmartWatch和SmartSpeaker都实现了Chargeable) - 接口的默认方法可以直接使用,也可以重写
- 通过接口类型的数组,可以把不同的对象统一处理
10. 接口的演进(从 Java 8 到 21)
了解接口的演进历史,能帮你理解它为什么是现在这个样子:
| 版本 | 接口的能力 |
|---|---|
| Java 7 | 只有 public abstract 方法和 public static final 常量 |
| Java 8 | 新增默认方法和静态方法 |
| Java 9 | 新增私有方法(用于辅助默认方法) |
| Java 14 | @FunctionalInterface 和 Lambda 的深度结合 |
| Java 17+ | 密封接口(Sealed Interfaces) |
接口已经从“纯粹的抽象”进化成了“带有部分实现的抽象”,但它的核心使命从未改变:定义契约,解耦实现。
11. 今天的总结
今天我们全面学习了接口:
- 接口:一份纯粹的“契约”,只定义方法签名,不提供实现(除默认方法外)。
interface/implements:定义接口和实现接口的关键字。- 多实现:一个类可以实现多个接口,弥补单继承的不足。
- 默认方法:Java 8 引入,让接口可以平滑演化。
- 静态方法:Java 8 引入,提供接口相关的工具方法。
- 函数式接口:只有一个抽象方法的接口,是 Lambda 表达式的基础。
- 密封接口:JDK 17+ 支持,限制哪些类可以实现该接口。
- 接口 vs 抽象类:接口定义“能做什么”,抽象类定义“是什么”。
接口让你的代码变得更灵活、更解耦、更易于测试和扩展。在实际开发中,接口无处不在——从 JDK 的 List、Map,到 Spring 的 Repository、Service,再到各种设计模式,接口都是核心的“粘合剂”。
动手试试
-
定义一个接口
Playable,包含play()方法。然后定义三个类:MusicPlayer、VideoPlayer、GameConsole,分别实现Playable接口。在main中用一个Playable数组统一管理,循环调用play()。 -
定义一个接口
Drawable,包含draw()方法。再定义一个抽象类Shape(包含color属性和getArea()抽象方法)。让Circle、Rectangle同时继承Shape并实现Drawable。这样既有了“是什么”(图形),又有了“能做什么”(可绘制)。 -
写一个“支付系统”:定义接口
Payment,包含pay(double amount)方法。然后定义CreditCardPayment、WeChatPayment、AlipayPayment三个实现类。在main中写一个方法processPayment(Payment p, double amount),接受任何支付方式,调用pay()。 -
(挑战)结合第 16 篇的计算器项目,用接口替代抽象类重构运算逻辑。定义一个
Operation接口(只有double calculate(double a, double b)方法),然后为每种运算创建实现类。最后用Map<String, Operation>存储所有运算,在菜单中动态调用。这样新增运算只需要新增一个实现类并在 Map 中注册,完全不需要修改主逻辑。
接口是 Java 面向对象设计中最强大的工具之一,熟练掌握它是成为优秀 Java 开发者的必经之路。多写、多想、多体会,你会发现接口无处不在。
我们下一篇会讲内部类——一种定义在其他类内部的类。它虽然不如接口那么“显眼”,但在某些场景下非常有用。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。
450



被折叠的 条评论
为什么被折叠?



