跳到主内容
创建时间: 2026-09-16最后更新: 2026-09-16

前面的章节已经学习了类如何保存状态、通过构造方法建立合法对象,以及不同包之间如何使用公开类型。不过,当一个类直接依赖另一个具体类时,它也会同时依赖对方的实现细节。以后想替换实现,调用方往往也要跟着修改。

Java 接口用来声明一组可以被不同类共同实现的行为。多态则允许调用方只依赖这组行为,在运行时与不同实现协作。本章先使用普通 Java 代码理解这两个概念,再说明它们怎样为 Spring 的依赖注入和后面的 Repository 铺路。

1. 接口

假设心跳服务需要决定当前返回什么状态。先声明一个只描述能力的接口:

src/main/java/cn/usehook/spring/basics/HealthStatusProvider.java
1
package cn.usehook.spring.basics;
2
3
public interface HealthStatusProvider {
4
5
String currentStatus();
6
}

interface 声明的是类型和契约,而不是可以直接创建的完整对象。这里的契约是:任何 HealthStatusProvider 实现都必须能够返回当前状态。

接口方法默认就是 public abstract,所以上面的声明等价于:

HealthStatusProvider.java
1
public abstract String currentStatus();

通常不重复写这两个修饰符。接口也不能保存每个对象各自变化的普通实例字段;如果在接口中声明字段,它会隐式成为 public static final 常量。业务状态仍然应该由实现类自己的字段保存。

接口文件遵守普通 Java 类型的规则:公开接口 HealthStatusProvider 应放在同名的 HealthStatusProvider.java 中,文件所在目录应与 package cn.usehook.spring.basics 对应。其他包使用它时,通过 import 引入这个公开类型。

2. 实现类

类使用 implements 表示自己履行接口契约:

src/main/java/cn/usehook/spring/basics/ConfiguredHealthStatusProvider.java
01
package cn.usehook.spring.basics;
02
03
public class ConfiguredHealthStatusProvider implements HealthStatusProvider {
04
05
private final String status;
06
07
public ConfiguredHealthStatusProvider(String status) {
08
this.status = status;
09
}
10
11
@Override
12
public String currentStatus() {
13
return status;
14
}
15
}

实现类必须提供接口要求的方法。接口方法对外公开,因此实现方法也必须是 public,不能缩小访问范围。

@Override 会让编译器确认当前方法确实覆盖了接口方法。如果拼错方法名、参数不同或者返回类型不兼容,编译器会直接指出问题。它不是让覆盖行为生效的开关,但应该保留这种检查。

现在可以创建实现类对象,并通过它调用方法:

interface-main.java
1
ConfiguredHealthStatusProvider provider =
2
new ConfiguredHealthStatusProvider("UP");
3
4
System.out.println(provider.currentStatus());

接口本身不能使用 new HealthStatusProvider() 创建,因为它没有给出 currentStatus() 的具体实现。真正被创建的始终是实现类对象。

3. 接口引用与多态

实现类对象可以赋给接口类型的变量:

interface-reference.java
1
HealthStatusProvider provider =
2
new ConfiguredHealthStatusProvider("UP");

变量的声明类型是 HealthStatusProvider,实际对象类型是 ConfiguredHealthStatusProvider。编译器只允许通过这个变量调用接口公开的能力;运行时则执行实际对象提供的方法实现。

再增加一个不同实现:

src/main/java/cn/usehook/spring/basics/MaintenanceHealthStatusProvider.java
1
package cn.usehook.spring.basics;
2
3
public class MaintenanceHealthStatusProvider implements HealthStatusProvider {
4
5
@Override
6
public String currentStatus() {
7
return "MAINTENANCE";
8
}
9
}

下面的方法不需要知道状态来自配置还是维护模式:

status-message.java
1
static String createMessage(HealthStatusProvider provider) {
2
return "service status: " + provider.currentStatus();
3
}

两个实现都可以传入:

polymorphism.java
1
String normal = createMessage(
2
new ConfiguredHealthStatusProvider("UP")
3
);
4
String maintenance = createMessage(
5
new MaintenanceHealthStatusProvider()
6
);

同一段调用代码能够与多个实际类型协作,这就是当前场景中的多态。它不是绕过类型检查,而是让多个类通过共同接口参与同一种调用。

4. 依赖接口

类可以在构造方法中声明自己依赖接口:

src/main/java/cn/usehook/spring/basics/HealthMessageService.java
01
package cn.usehook.spring.basics;
02
03
public class HealthMessageService {
04
05
private final HealthStatusProvider statusProvider;
06
07
public HealthMessageService(HealthStatusProvider statusProvider) {
08
this.statusProvider = statusProvider;
09
}
10
11
public String message() {
12
return "service status: " + statusProvider.currentStatus();
13
}
14
}

HealthMessageService 只要求构造参数满足 HealthStatusProvider 契约,不负责创建或选择具体实现。普通 Java 程序可以在组合对象的位置作出选择:

dependency-selection.java
1
HealthStatusProvider provider = maintenanceMode
2
? new MaintenanceHealthStatusProvider()
3
: new ConfiguredHealthStatusProvider("UP");
4
5
HealthMessageService service = new HealthMessageService(provider);

这正是构造器依赖注入所需要的类型基础。后面的 IoC 容器会接管对象创建与选择,但 Java 语言层面发生的事情仍然是:把一个满足接口类型的对象传给构造方法。

如果同时把多个实现注册为 Spring Bean,容器还需要知道应该注入哪一个。@Primary、@Qualifier 和候选解析会解决选择问题;接口本身不会自动决定默认实现。

5. 接口与抽象类

接口和抽象类都不能代表一个完整的最终实现,但用途不同:

对比接口抽象类
主要目的声明共同能力复用同一类对象的状态与基础实现
继承数量一个类可以实现多个接口一个类只能继承一个直接父类
实例字段不保存普通实例字段可以保存实例字段
构造方法没有实例构造方法可以定义构造方法供子类调用
适用关系不同类型具备同一种能力多个类属于同一继承体系

接口也可以包含 default 方法,为已有契约补充默认行为:

HealthStatusProvider.java
1
public interface HealthStatusProvider {
2
3
String currentStatus();
4
5
default boolean isAvailable() {
6
return "UP".equals(currentStatus());
7
}
8
}

默认方法应建立在接口自身的公开能力上。不要把大量共享状态和复杂流程都塞进接口;确实需要共享状态与受保护实现时,抽象类通常更合适。当前项目主要使用接口表达可替换边界,使用普通类实现业务行为。

6. 项目边界

并非每个类都要对应一个接口。当前 HealthService 只有一个清晰实现,Controller 也没有替换它的实际需求,仅为了形式创建 HealthServiceInterface 会增加文件和跳转,却没有形成真正的抽象边界。

后面的 HealthStatusRepository 则不同:它先使用内存保存状态,随后会替换成 PostgreSQL 实现。上层 Service 希望保持调用方式不变,因此 Repository 接口能够稳定表达 save、查询和列表等能力,两个实现分别处理不同存储技术。

接口也让普通测试可以传入轻量实现:

interface-test.java
1
HealthStatusProvider testProvider =
2
new ConfiguredHealthStatusProvider("DOWN");
3
HealthMessageService service = new HealthMessageService(testProvider);
4
5
assert service.message().equals("service status: DOWN");

这里替换的是接口背后的实现,不需要修改 HealthMessageService,也不需要启动 Spring 容器。测试只组装当前行为所需的最小对象图。

接口解决了「调用方依赖什么契约」,但 Repository 还会返回 List<HealthStatus>、Optional<HealthStatus> 等带类型参数的对象。下一篇继续学习集合、泛型与 Optional,把这些方法签名需要的 Java 基础补齐后,再进入 Spring 容器。