Java 中惰性求值的潜能,完全被忽视了(在语言层面上,它仅被用来实现 短路求值 )。更先进的语言,如 Scala,区分了传值调用与传名调用,或者引入了 lazy 这样的关键字。
尽管 Java 8 通过延迟队列的实现(java.util.stream.Stream)在惰性求值的方面有些改进,但是我们会先跳过 Stream,而把重点放在如何使用 lambda 表达式实现一个轻量级的惰性求值。
基于 lambda 的惰性求值
Scala
当我们想对 Scala 中的方法参数进行惰性求值时,我们用“传名调用”来实现。
让我们创建一个简单的 foo 方法,它接受一个 String 示例,然后返回这个 String:
deffoo(b:String):String=b
一切都是马上返回的,跟 Java 中的一样。如果我们想让 b 的计算延迟,可以使用传名调用的语法,只要在 b 的类型声明上加两个符号,来看:
deffoo(b:=> String):String=b
如果用 javap 反编译上面生成的 *.class 文件,可以看到:
Compiled from "LazyFoo.scala"
publicfinalclassLazyFoo {
publicstaticjava.lang.String foo(scala.Function0);
Code:
0: getstatic #17 // Field LazyFoo.MODULE:LLazyFoo$;
3: aload_0
4: invokevirtual #19 // Method LazyFoo$.foo:(Lscala/Function0;)Ljava/lang/String;
7: areturn
}
看起来传给这个函数的参数不再是一个String了,而是变成了一个Function0,这使得对这个表达式进行延迟计算变得可能 —— 只要我们不去调用他,计算就不会被触发。Scala 中的惰性求值就是这么简单。
使用 Java
现在,如果我们需要延迟触发一个返回T的计算,我们可以复用上面的思路,将计算包装为一个返回Supplier实例的 Java Function0 :
Integer v1 = 42; // eager
Supplier<Integer> v2 = () -> 42; // lazy
如果需要花费较长时间才能从函数中获得结果,上面这个方法会更加实用:
Integer v1 = compute(); //eager
Supplier<Integer> value = () -> compute(); // lazy
同样的,这次传入一个方法作为参数:
privatestaticintcomputeLazily(Supplier value) {
// ...
}
如果仔细观察 Java 8 中新增的 API,你会注意到这种模式使用得特别频繁。一个最显著的例子就是 Optional#orElseGet ,Optional#orElse 的惰性求值版本。
如果不使用这种模式的话,那么 Optional 就没什么用处了… 或许吧。当然,我们不会满足于 suppliers 。我们可以用同样的方法复用所有 functional 接口。
线程安全和缓存
不幸的是,上面这个简单的方法是有缺陷的:每次调用都会触发一次计算。不仅多线程的调用有这个缺陷,同一个线程连续调用多次也有这个缺陷。不过,如果我们清楚这个缺陷,并且合理的使用这个技术,那就没什么问题。
使用缓存的惰性求值
刚才已经提到,基于 lambda 表达式的方法在一些情况下是有缺陷的,因为返回值没有保存起来。为了修复这个缺陷,我们需要构造一个专用的工具,让我们叫它 Lazy :
publicclassLazy { ... }
这个工具需要自身同时保存Supplier和 返回值T。
@RequiredArgsConstructor
publicclassNaiveLazy {
privatefinalSupplier supplier;
privateT value;
publicT get() {
if(value ==null) {
value = supplier.get();
}
returnvalue;
}
}
就是这么简单。注意上面的代码仅仅是一个概念模型,暂时还不是线程安全的。
幸运的是,如果想让它变得线程安全,只需要保证不同的线程在获取返回值的时候不会触发同样的计算。这可以简单的通过双重检查锁定机制来实现(我们不能直接在 get() 方法上加锁,这会引入不必要的竞争):
@RequiredArgsConstructor
publicclassLazy {
privatefinalSupplier supplier;
privatevolatileT value;
publicT get() {
if(value ==null) {
synchronized(this) {
if(value ==null) {
value = supplier.get();
}
}
}
returnvalue;
}
}
现在,我们有了一个完整的 Java 惰性求值的函数化实现。由于它不是在语言的层面实现的,需要付出创建一个新对象的代价。
更深入的讨论
当然,我们不会就此打住,我们可以进一步的优化这个工具。比如,通过引入一个惰性的filter()/flatMap()/map()方法,可以让它使用起来更加流畅,并且组合性更强:
public Lazy map(Function mapper) {
returnnewLazy<>(() -> mapper.apply(this.get()));
}
public Lazy flatMap(Function> mapper) {
returnnewLazy<>(() -> mapper.apply(this.get()).get());
}
作者:Java大生
链接:https://www.jianshu.com/p/fe009908f32c
共同学习,写下你的评论
评论加载中...
作者其他优质文章