本文目录导读:

- 场景一:延迟加载 (Lazy Initialization)
- 场景二:创建复杂或可变的对象工厂
- 场景三:作为方法的参数或返回值,传递“构造逻辑”
- 场景四:测试与模拟 (Mocking & Test Data)
- 对比:Supplier vs. 直接在代码中 new
我们来详细探讨一下 Supplier 在 Java (以及类似函数式语言中) 中提供对象实例的几种典型场景。
核心观点:Supplier<T> 的核心价值在于将“对象的创建逻辑”封装并延迟到真正需要的时刻执行,而不是在代码声明时就创建。
它常用于解决以下问题:延迟加载、创建复杂对象、动态策略、以及生成测试数据。
下面我们分场景进行详细说明。
延迟加载 (Lazy Initialization)
这是最经典的场景,对象创建代价高(如数据库连接、网络资源、大型配置对象),且不一定在程序启动时就需要。Supplier 可以让你在真正调用 get() 时才去创建。
示例:创建数据库连接
import java.util.function.Supplier;
import java.sql.Connection;
import java.sql.DriverManager;
public class DatabaseService {
// 方式一:传统方式(在类初始化时就创建,非常昂贵,且可能失败)
// private static Connection connection = createExpensiveConnection();
// 方式二:使用 Supplier 进行延迟加载
private static Supplier<Connection> lazyConnectionSupplier = () -> {
System.out.println("+++ 即将创建真实的数据库连接 +++");
return createExpensiveConnection(); // 这个 lambda 在被 get() 调用时才执行
};
// 实际使用时,只会创建一次(需要额外处理单例)
private static Connection connection = null;
public static Connection getConnection() {
if (connection == null) {
connection = lazyConnectionSupplier.get(); // 首次调用才创建
}
return connection;
}
private static Connection createExpensiveConnection() {
// 模拟一个耗时操作,比如创建数据库连接
System.out.println("正在建立数据库连接...");
// 实际代码中: return DriverManager.getConnection(url, user, password);
return null; // 简化
}
public static void main(String[] args) {
System.out.println("程序启动,但还没有调用连接");
// 当业务真正需要时才触发创建
Connection conn = getConnection();
System.out.println("获取到连接:" + conn);
Connection conn2 = getConnection(); // 这次返回的是同一个对象(懒加载单例)
System.out.println("第二次获取:" + conn2);
}
}
输出:
程序启动,但还没有调用连接
+++ 即将创建真实的数据库连接 +++
正在建立数据库连接...
获取到连接:null
第二次获取:null
优势: 避免了不必要的资源开销和初始化失败的风险,如果应用启动后,这个连接从未被使用,那么这个昂贵操作就永远不会执行。
创建复杂或可变的对象工厂
当一个对象的创建需要多个步骤、配置参数,或者对象创建逻辑会随着上下文改变时,Supplier 可以充当一个灵活的对象工厂。
示例:创建不同策略的报表生成器
import java.util.function.Supplier;
interface ReportGenerator {
void generate();
}
class PDFReportGenerator implements ReportGenerator {
public void generate() {
System.out.println("生成 PDF 报表");
}
}
class CSVReportGenerator implements ReportGenerator {
public void generate() {
System.out.println("生成 CSV 报表");
}
}
public class ReportService {
// Supplier 作为工厂的抽象
public void generateReport(Supplier<ReportGenerator> generatorSupplier) {
System.out.println("准备报表环境...");
ReportGenerator generator = generatorSupplier.get(); // 由调用方决定创建哪种报表
generator.generate();
System.out.println("报表生成完成。");
}
public static void main(String[] args) {
ReportService service = new ReportService();
// 根据用户选择,传入不同的供应商
String reportType = "PDF"; // 假设从配置或用户输入获取
Supplier<ReportGenerator> supplier;
if ("PDF".equals(reportType)) {
supplier = PDFReportGenerator::new; // 方法引用,等同于 () -> new PDFReportGenerator()
} else {
supplier = CSVReportGenerator::new;
}
service.generateReport(supplier);
}
}
优势: 将对象的创建(供应)与使用(消费)完全解耦。ReportService 不需要知道具体实现类,只需接收一个能提供对象的 Supplier。
作为方法的参数或返回值,传递“构造逻辑”
当你想在方法签名中表达“我需要一个由你负责创建的对象,但创建时机由我决定”时,Supplier 是最佳选择。
示例:通用的异常重试机制
import java.util.function.Supplier;
public class RetryUtil {
// 接收一个 Supplier,尝试 3 次获取对象,成功则返回,否则抛出异常
public static <T> T retryOnFailure(Supplier<T> supplier, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
return supplier.get(); // 尝试创建或调用核心逻辑
} catch (Exception e) {
System.out.println("第 " + (i + 1) + " 次尝试失败: " + e.getMessage());
if (i == maxRetries - 1) {
throw new RuntimeException("所有重试都已失败", e);
}
// 可以在这里添加一些等待逻辑
}
}
return null; // 不会执行到这里
}
public static void main(String[] args) {
// 调用重试机制,传入一个可能会失败的复杂对象的创建逻辑
String result = retryOnFailure(() -> {
// 假设这是一个可能因为网络问题而失败的 API 调用
return callExternalAPI();
}, 3);
System.out.println("最终结果: " + result);
}
private static String callExternalAPI() {
// 模拟:第一次失败,第二次成功
if (Math.random() > 0.5) {
System.out.println("API 调用成功");
return "Success Data";
} else {
throw new RuntimeException("Network Error");
}
}
}
优势: retryOnFailure 方法只关心如何重试,不关心具体创建什么对象或执行什么逻辑,调用方通过 Supplier 注入具体的行为。
测试与模拟 (Mocking & Test Data)
在单元测试中,Supplier 非常有用,可以轻松地创建一个对象生成器,用于快速生成测试数据。
示例:生成测试用的用户对象
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Stream;
class User {
private String id;
private String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User{" + "id='" + id + '\'' + ", name='" + name + '\'' + '}';
}
}
public class TestDataFactory {
public static void main(String[] args) {
// 创建一个 Supplier 来生成默认的用户(懒加载)
Supplier<User> defaultUserSupplier = () -> new User("default-id", "Default User");
// 创建一个更有趣的 Supplier:每次生成一个随机的用户
Supplier<User> randomUserSupplier = () -> new User(
UUID.randomUUID().toString().substring(0, 8),
"User_" + (int)(Math.random() * 1000)
);
// 在测试中,你可以使用它来生成数据流
System.out.println("默认用户(只创建一次):" + defaultUserSupplier.get());
System.out.println("\n随机用户列表:");
Stream.generate(randomUserSupplier) // 无限生成
.limit(5)
.forEach(System.out::println);
}
}
优势: 快速、灵活地生成测试数据,特别是当对象的构造函数复杂或需要重复创建时,结合 Stream.generate() 可以轻松生成无限的测试数据流。
对比:Supplier vs. 直接在代码中 new
| 特性 | 使用 Supplier |
直接 new |
|---|---|---|
| 控制时机 | 延迟到 .get() 调用时 |
立即执行 |
| 传递逻辑 | 可将创建逻辑作为参数传递 | 无法传递,除非使用工厂模式 |
| 解耦程度 | 高,调用者不关心如何创建 | 低,调用者需要知道具体类 |
| 代码表现力 | 更函数式,声明“我需要一个这个” | 更命令式,声明“我现在就要这个” |
| 适用场景 | 复杂工厂、延迟加载、重试、测试数据 | 简单/直接的对象创建 |
Supplier 的核心场景是当你需要将“如何得到某个实例”作为一个可传递、可延迟执行的数据或被调用的逻辑时,它比直接调用构造方法提供了更高层次的抽象和灵活性,是函数式编程思想在对象创建领域的完美体现。
- 延迟加载:减少启动/初始化开销。
- 对象工厂:灵活创建不同策略的实例。
- 方法参数:作为回调,将对象的创建逻辑注入到另一个方法中。
- 测试数据:快速生成测试对象流。