前面的章节已经学习了类如何保存状态、通过构造方法建立合法对象,以及不同包之间如何使用公开类型。不过,当一个类直接依赖另一个具体类时,它也会同时依赖对方的实现细节。以后想替换实现,调用方往往也要跟着修改。
Java 接口用来声明一组可以被不同类共同实现的行为。多态则允许调用方只依赖这组行为,在运行时与不同实现协作。本章先使用普通 Java 代码理解这两个概念,再说明它们怎样为 Spring 的依赖注入和后面的 Repository 铺路。
1. 接口
假设心跳服务需要决定当前返回什么状态。先声明一个只描述能力的接口:
1package cn.usehook.spring.basics;23public interface HealthStatusProvider {45String currentStatus();6}
interface 声明的是类型和契约,而不是可以直接创建的完整对象。这里的契约是:任何 HealthStatusProvider 实现都必须能够返回当前状态。
接口方法默认就是 public abstract,所以上面的声明等价于:
1public abstract String currentStatus();
通常不重复写这两个修饰符。接口也不能保存每个对象各自变化的普通实例字段;如果在接口中声明字段,它会隐式成为 public static final 常量。业务状态仍然应该由实现类自己的字段保存。
接口文件遵守普通 Java 类型的规则:公开接口 HealthStatusProvider 应放在同名的 HealthStatusProvider.java 中,文件所在目录应与 package cn.usehook.spring.basics 对应。其他包使用它时,通过 import 引入这个公开类型。
2. 实现类
类使用 implements 表示自己履行接口契约:
01package cn.usehook.spring.basics;0203public class ConfiguredHealthStatusProvider implements HealthStatusProvider {0405private final String status;0607public ConfiguredHealthStatusProvider(String status) {08this.status = status;09}1011@Override12public String currentStatus() {13return status;14}15}
实现类必须提供接口要求的方法。接口方法对外公开,因此实现方法也必须是 public,不能缩小访问范围。
@Override 会让编译器确认当前方法确实覆盖了接口方法。如果拼错方法名、参数不同或者返回类型不兼容,编译器会直接指出问题。它不是让覆盖行为生效的开关,但应该保留这种检查。
现在可以创建实现类对象,并通过它调用方法:
1ConfiguredHealthStatusProvider provider =2new ConfiguredHealthStatusProvider("UP");34System.out.println(provider.currentStatus());
接口本身不能使用 new HealthStatusProvider() 创建,因为它没有给出 currentStatus() 的具体实现。真正被创建的始终是实现类对象。
3. 接口引用与多态
实现类对象可以赋给接口类型的变量:
1HealthStatusProvider provider =2new ConfiguredHealthStatusProvider("UP");
变量的声明类型是 HealthStatusProvider,实际对象类型是 ConfiguredHealthStatusProvider。编译器只允许通过这个变量调用接口公开的能力;运行时则执行实际对象提供的方法实现。
再增加一个不同实现:
1package cn.usehook.spring.basics;23public class MaintenanceHealthStatusProvider implements HealthStatusProvider {45@Override6public String currentStatus() {7return "MAINTENANCE";8}9}
下面的方法不需要知道状态来自配置还是维护模式:
1static String createMessage(HealthStatusProvider provider) {2return "service status: " + provider.currentStatus();3}
两个实现都可以传入:
1String normal = createMessage(2new ConfiguredHealthStatusProvider("UP")3);4String maintenance = createMessage(5new MaintenanceHealthStatusProvider()6);
同一段调用代码能够与多个实际类型协作,这就是当前场景中的多态。它不是绕过类型检查,而是让多个类通过共同接口参与同一种调用。
4. 依赖接口
类可以在构造方法中声明自己依赖接口:
01package cn.usehook.spring.basics;0203public class HealthMessageService {0405private final HealthStatusProvider statusProvider;0607public HealthMessageService(HealthStatusProvider statusProvider) {08this.statusProvider = statusProvider;09}1011public String message() {12return "service status: " + statusProvider.currentStatus();13}14}
HealthMessageService 只要求构造参数满足 HealthStatusProvider 契约,不负责创建或选择具体实现。普通 Java 程序可以在组合对象的位置作出选择:
1HealthStatusProvider provider = maintenanceMode2? new MaintenanceHealthStatusProvider()3: new ConfiguredHealthStatusProvider("UP");45HealthMessageService service = new HealthMessageService(provider);
这正是构造器依赖注入所需要的类型基础。后面的 IoC 容器会接管对象创建与选择,但 Java 语言层面发生的事情仍然是:把一个满足接口类型的对象传给构造方法。
如果同时把多个实现注册为 Spring Bean,容器还需要知道应该注入哪一个。@Primary、@Qualifier 和候选解析会解决选择问题;接口本身不会自动决定默认实现。
5. 接口与抽象类
接口和抽象类都不能代表一个完整的最终实现,但用途不同:
| 对比 | 接口 | 抽象类 |
|---|---|---|
| 主要目的 | 声明共同能力 | 复用同一类对象的状态与基础实现 |
| 继承数量 | 一个类可以实现多个接口 | 一个类只能继承一个直接父类 |
| 实例字段 | 不保存普通实例字段 | 可以保存实例字段 |
| 构造方法 | 没有实例构造方法 | 可以定义构造方法供子类调用 |
| 适用关系 | 不同类型具备同一种能力 | 多个类属于同一继承体系 |
接口也可以包含 default 方法,为已有契约补充默认行为:
1public interface HealthStatusProvider {23String currentStatus();45default boolean isAvailable() {6return "UP".equals(currentStatus());7}8}
默认方法应建立在接口自身的公开能力上。不要把大量共享状态和复杂流程都塞进接口;确实需要共享状态与受保护实现时,抽象类通常更合适。当前项目主要使用接口表达可替换边界,使用普通类实现业务行为。
6. 项目边界
并非每个类都要对应一个接口。当前 HealthService 只有一个清晰实现,Controller 也没有替换它的实际需求,仅为了形式创建 HealthServiceInterface 会增加文件和跳转,却没有形成真正的抽象边界。
后面的 HealthStatusRepository 则不同:它先使用内存保存状态,随后会替换成 PostgreSQL 实现。上层 Service 希望保持调用方式不变,因此 Repository 接口能够稳定表达 save、查询和列表等能力,两个实现分别处理不同存储技术。
接口也让普通测试可以传入轻量实现:
1HealthStatusProvider testProvider =2new ConfiguredHealthStatusProvider("DOWN");3HealthMessageService service = new HealthMessageService(testProvider);45assert service.message().equals("service status: DOWN");
这里替换的是接口背后的实现,不需要修改 HealthMessageService,也不需要启动 Spring 容器。测试只组装当前行为所需的最小对象图。
接口解决了「调用方依赖什么契约」,但 Repository 还会返回 List<HealthStatus>、Optional<HealthStatus> 等带类型参数的对象。下一篇继续学习集合、泛型与 Optional,把这些方法签名需要的 Java 基础补齐后,再进入 Spring 容器。