- 0
- 171 words
抽象类讲完了。你知道父类可以只声明方法签名,不写实现体,强制子类去实现。这是一种 「模板」 的思想。
但抽象类有个问题:一个类 只能继承一个父类。如果某个类想要拥有多种能力——比如一个汽车类想同时具备「可驾驶」和「可充电」的特征——单继承的局限性就卡住了。
Java 的解决方案就是 接口(Interface)。接口只定义了「能做什么」的契约,不关心「怎么做」。而且一个类可以实现多个接口,弥补了单继承的不足。
抽象类是「是什么」的关系(is-a),接口是「能做什么」的关系(can-do)。
一、接口的语法
1.1 定义和实现一个接口
// 定义接口——用 interface 关键字,而不是 class
public interface Flyable {
// 抽象方法(不写 abstract 也默认是)
void fly();
}
// 实现接口——用 implements 关键字
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("鸟儿扇动翅膀飞行");
}
}
class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("飞机引擎推进飞行");
}
}
1.2 接口与多态
接口天然支持多态——你可以声明一个接口类型的引用,指向任何实现了该接口的类对象:
Flyable f1 = new Bird();
Flyable f2 = new Airplane();
f1.fly(); // 鸟儿扇动翅膀飞行——动态绑定
f2.fly(); // 飞机引擎推进飞行——动态绑定
// 多态数组
Flyable[] sky = { new Bird(), new Airplane(), new Drone() };
for (Flyable f : sky) {
f.fly();
}
有没有发现和抽象类很像?是的,接口的引用和抽象类的引用一样,都是靠多态在运行期决定调用哪个实现类的方法。区别在于——接口更「纯粹」,只定义行为契约,不包含任何状态。
二、接口的特性与规则
2.1 接口中的成员
| 成员类型 | 默认修饰符 | 备注 |
|---|---|---|
| 抽象方法 | public abstract | 接口的主要成员,实现类必须重写 |
| 常量 | public static final | 接口中的变量默认都是常量 |
| 默认方法(Java 8+) | public default | 接口可以有默认实现,子类可选择是否重写 |
| 静态方法(Java 8+) | public static | 通过接口名直接调用 |
| 私有方法(Java 9+) | private | 辅助默认方法/静态方法,对外不可见 |
public interface AdvancedInterface {
// 1. 抽象方法(最常见)
void doSomething();
// 2. 常量(接口中的变量都是 public static final)
int MAX_COUNT = 100;
// 3. 默认方法——有方法体,子类可选择是否重写
default void doDefault() {
System.out.println("默认实现,子类可以不重写");
helperMethod(); // 调私有方法
}
// 4. 静态方法——通过接口名调用
static void doStatic() {
System.out.println("接口自己的静态方法");
}
// 5. 私有方法(JDK 9+)——给默认方法或静态方法内部复用
private void helperMethod() {
System.out.println("私有辅助方法,对外不可见");
}
}
2.2 接口不能包含的东西
- ❌ 实例变量(接口没有状态,不能有非静态字段)
- ❌ 构造器(接口不能被实例化,当然也没有构造器)
- ❌ 实例初始化块(没有实例,初始化块自然没意义)
2.3 接口的多继承
接口之间可以 多继承:
interface A {
void methodA();
}
interface B {
void methodB();
}
// 接口多继承
interface C extends A, B {
void methodC();
}
// 实现类——必须实现所有接口中的所有抽象方法
class MyClass implements C {
@Override
public void methodA() { /* ... */ }
@Override
public void methodB() { /* ... */ }
@Override
public void methodC() { /* ... */ }
}
这是接口和抽象类的重要区别之一——抽象类只能单继承,接口可以多继承。但因为接口没有状态(没有实例变量),多继承不会引入「菱形问题」中的状态冲突。方法签名上的冲突可以通过重写或 super 语法解决。
三、接口 vs 抽象类
这是面试必问题,同时也是实际开发中最容易纠结的点。
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class | interface |
| 关系 | is-a(是什么) | can-do(能做什么) |
| 多继承 | 只能单继承 | 可以多实现、多继承 |
| 构造器 | 有 | 没有 |
| 实例变量 | 可以有 | 不能有(只有常量) |
| 方法实现 | 可以有普通方法+抽象方法 | 全抽象方法(Java 8+可以有默认/静态方法) |
| 访问修饰符 | 可以用任意访问修饰符 | 方法默认 public |
| 使用场景 | 类族有共同基类,共享状态和部分实现 | 定义行为契约,不关心具体层级 |
3.1 选择判断器
纠结的时候,问自己三个问题:
- 这两个类之间有「是什么」的关系吗?(Cat is-a Animal → 用抽象类)
- 多个不相关的类需要共用同一套行为吗?(Bird 和 Airplane 都能 fly → 用接口)
- 需要共享状态(字段)吗?(需要 → 用抽象类;不需要 → 用接口)
通常情况下,行业的共识是:优先使用接口,而不是抽象类。接口的耦合度更低,更灵活,也更容易测试和 mock。
四、default 方法的引入动机
Java 8 之前,接口里只能有抽象方法。这意味着如果给一个接口新增一个方法,所有实现类都得跟着改——即便大多数实现类的实现逻辑都一样。
Java 8 引入 default 方法的初衷:
- 为了给
Collection接口新增stream()、forEach()等方法,但又不强制所有实现类重写 - 如果不用 default 方法,Java 8 要加流式操作,就得让所有集合类(ArrayList、HashSet 等)都改代码——那场面太美不敢看
// Collection 接口中 Java 8 新增的默认方法
public interface Collection<E> {
// 这个方法所有实现类自动就有了,不用逐个重写
default void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (E e : this) {
action.accept(e);
}
}
}
你看,ArrayList、LinkedList、HashSet……所有这些集合类,一行代码没改就拥有了 forEach() 方法。这就是 default 方法的威力。
五、总结
- 接口是「契约」:定义行为规范,不涉及具体实现和状态
- 接口支持多实现:弥补 Java 单继承的不足
- 接口的成员:抽象方法(主流)+ 常量 + default 方法 + 静态方法 + 私有方法
- 接口 vs 抽象类:is-a 用抽象类,can-do 用接口;优先接口
- default 方法:让接口可以扩展而不破坏现有实现
这一篇是接口的基础。下一篇我们来深入接口在工程中的高级用法——标记接口、函数式接口(lambda 的前置知识)、接口隔离原则、回调与策略模式。你会发现,理解了接口,才能真正理解现代 Java 生态。