1. 引言
在处理需要执行昂贵或慢速方法的资源时(如数据库查询或REST调用),我们通常会使用本地缓存或私有字段。Lambda函数允许我们将方法作为参数传递,从而延迟方法执行或完全省略执行。
本文将展示如何使用Lambda函数实现字段的延迟初始化。
2. Lambda替代方案
首先实现基础版本。创建LambdaSupplier
类:
public class LambdaSupplier<T> {
protected final Supplier<T> expensiveData;
public LambdaSupplier(Supplier<T> expensiveData) {
this.expensiveData = expensiveData;
}
public T getData() {
return expensiveData.get();
}
}
LambdaSupplier
通过延迟执行Supplier.get()
实现字段延迟初始化。**但每次调用getData()
都会触发Supplier.get()
**,行为与Supplier
接口完全一致。
验证该行为的单元测试:
@Test
public void whenCalledMultipleTimes_thenShouldBeCalledMultipleTimes() {
@SuppressWarnings("unchecked") Supplier<String> mockedExpensiveFunction = Mockito.mock(Supplier.class);
Mockito.when(mockedExpensiveFunction.get())
.thenReturn("expensive call");
LambdaSupplier<String> testee = new LambdaSupplier<>(mockedExpensiveFunction);
Mockito.verify(mockedExpensiveFunction, Mockito.never())
.get();
testee.getData();
testee.getData();
Mockito.verify(mockedExpensiveFunction, Mockito.times(2))
.get();
}
测试验证Supplier.get()
被调用了两次,符合预期。
3. 延迟Supplier
为解决多次调用问题,升级实现保证昂贵方法只执行一次。LazyLambdaSupplier
扩展LambdaSupplier
,增加缓存字段:
public class LazyLambdaSupplier<T> extends LambdaSupplier<T> {
private T data;
public LazyLambdaSupplier(Supplier<T> expensiveData) {
super(expensiveData);
}
@Override
public T getData() {
if (data != null) {
return data;
}
return data = expensiveData.get();
}
}
实现将返回值存储在私有字段data
中,后续调用直接复用。
验证单次调用的测试:
@Test
public void whenCalledMultipleTimes_thenShouldBeCalledOnlyOnce() {
@SuppressWarnings("unchecked") Supplier<String> mockedExpensiveFunction = Mockito.mock(Supplier.class);
Mockito.when(mockedExpensiveFunction.get())
.thenReturn("expensive call");
LazyLambdaSupplier<String> testee = new LazyLambdaSupplier<>(mockedExpensiveFunction);
Mockito.verify(mockedExpensiveFunction, Mockito.never())
.get();
testee.getData();
testee.getData();
Mockito.verify(mockedExpensiveFunction, Mockito.times(1))
.get();
}
测试验证函数仅被调用一次。
⚠️ 非线程安全验证:
@Test
public void whenCalledMultipleTimesConcurrently_thenShouldBeCalledMultipleTimes() throws InterruptedException {
@SuppressWarnings("unchecked") Supplier mockedExpensiveFunction = Mockito.mock(Supplier.class);
Mockito.when(mockedExpensiveFunction.get())
.thenAnswer((Answer) invocation -> {
Thread.sleep(1000L);
return "Late response!";
});
LazyLambdaSupplier testee = new LazyLambdaSupplier<>(mockedExpensiveFunction);
Mockito.verify(mockedExpensiveFunction, Mockito.never())
.get();
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.invokeAll(List.of(testee::getData, testee::getData));
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
Mockito.verify(mockedExpensiveFunction, Mockito.times(2))
.get();
}
并发测试显示Supplier.get()
被调用两次。Thread.sleep()
确保两个线程同时进入getData()
时data
仍为null
。
4. 线程安全方案
使用同步访问和AtomicReference
解决线程安全问题:
public class LazyLambdaThreadSafeSupplier<T> extends LambdaSupplier<T> {
private final AtomicReference<T> data;
public LazyLambdaThreadSafeSupplier(Supplier<T> expensiveData) {
super(expensiveData);
data = new AtomicReference<>();
}
public T getData() {
if (data.get() == null) {
synchronized (data) {
if (data.get() == null) {
data.set(expensiveData.get());
}
}
}
return data.get();
}
}
双重检查锁定解析:
- 外层检查:避免初始化后线程阻塞在同步块
- 内层检查:确保只有一个线程执行初始化
验证线程安全的测试:
@Test
public void whenCalledMultipleTimesConcurrently_thenShouldBeCalledOnlyOnce() throws InterruptedException {
@SuppressWarnings("unchecked") Supplier mockedExpensiveFunction = Mockito.mock(Supplier.class);
Mockito.when(mockedExpensiveFunction.get())
.thenAnswer((Answer) invocation -> {
Thread.sleep(1000L);
return "Late response!";
});
LazyLambdaThreadSafeSupplier testee = new LazyLambdaThreadSafeSupplier<>(mockedExpensiveFunction);
Mockito.verify(mockedExpensiveFunction, Mockito.never())
.get();
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.invokeAll(List.of(testee::getData, testee::getData));
executorService.shutdown();
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
Mockito.verify(mockedExpensiveFunction, Mockito.times(1))
.get();
}
测试验证并发场景下函数仅执行一次。
5. 总结
本文展示了使用Lambda函数实现字段延迟初始化的多种方案:
- 基础方案:简单但多次执行
- 延迟方案:缓存结果但非线程安全
- 线程安全方案:双重检查锁定保证单次执行
这些方案可作为本地缓存或Project Lombok的@Getter(lazy=true)的替代实现。完整代码示例可在GitHub获取。