Java 中惰性求值的潜能,彻底被忽视了(在语言层面上,它仅被用来实现 短路求值 )。更先进的语言,如 Scala,区分了传值调用与传名调用,或者引入了 lazy 这样的关键字。java
尽管 Java 8 经过延迟队列的实现(java.util.stream.Stream)在惰性求值的方面有些改进,可是咱们会先跳过 Stream,而把重点放在如何使用 lambda 表达式实现一个轻量级的惰性求值。面试
基于 lambda 的惰性求值缓存
Scala安全
当咱们想对 Scala 中的方法参数进行惰性求值时,咱们用“传名调用”来实现。多线程
让咱们建立一个简单的 foo 方法,它接受一个 String 示例,而后返回这个 String:架构
def foo(b: String): String = bapp
一切都是立刻返回的,跟 Java 中的同样。若是咱们想让 b 的计算延迟,可使用传名调用的语法,只要在 b 的类型声明上加两个符号,来看:函数
def foo(b: => String): String = b工具
若是用 javap 反编译上面生成的 *.class 文件,能够看到:学习
Compiled from "LazyFoo.scala"
public final class LazyFoo {
public static java.lang.String foo(scala.Function0<java.lang.String>);
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
一样的,此次传入一个方法做为参数:
private static int computeLazily(Supplier<Integer> value) {
// ...
}
若是仔细观察 Java 8 中新增的 API,你会注意到这种模式使用得特别频繁。一个最显著的例子就是 Optional#orElseGet ,Optional#orElse 的惰性求值版本。
若是不使用这种模式的话,那么 Optional 就没什么用处了… 或许吧。固然,咱们不会知足于 suppliers 。咱们能够用一样的方法复用全部 functional 接口。
线程安全和缓存
不幸的是,上面这个简单的方法是有缺陷的:每次调用都会触发一次计算。不只多线程的调用有这个缺陷,同一个线程连续调用屡次也有这个缺陷。不过,若是咱们清楚这个缺陷,而且合理的使用这个技术,那就没什么问题。
使用缓存的惰性求值
刚才已经提到,基于 lambda 表达式的方法在一些状况下是有缺陷的,由于返回值没有保存起来。为了修复这个缺陷,咱们须要构造一个专用的工具,让咱们叫它 Lazy :
public class Lazy<T> { ... }
这个工具须要自身同时保存 Supplier 和 返回值 T。
@RequiredArgsConstructor
public class NaiveLazy<T> {
private final Supplier<T> supplier;
private T value;
public T get() {
if (value == null) {
value = supplier.get();
}
return value;
}
}
就是这么简单。注意上面的代码仅仅是一个概念模型,暂时还不是线程安全的。
幸运的是,若是想让它变得线程安全,只须要保证不一样的线程在获取返回值的时候不会触发一样的计算。这能够简单的经过双重检查锁定机制来实现(咱们不能直接在 get() 方法上加锁,这会引入没必要要的竞争):
@RequiredArgsConstructor
public class Lazy<T> {
private final Supplier<T> supplier;
private volatile T value;
public T get() {
if (value == null) {
synchronized (this) {
if (value == null) {
value = supplier.get();
}
}
}
return value;
}
}
如今,咱们有了一个完整的 Java 惰性求值的函数化实现。因为它不是在语言的层面实现的,须要付出建立一个新对象的代价。
更深刻的讨论
固然,咱们不会就此打住,咱们能够进一步的优化这个工具。好比,经过引入一个惰性的 filter()/flatMap()/map() 方法,可让它使用起来更加流畅,而且组合性更强:
public <R> Lazy<R> map(Function<T, R> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()));
}
public <R> Lazy<R> flatMap(Function<T, Lazy<R>> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()).get());
}
public Lazy<Optional<T>> filter(Predicate<T> predicate) {
return new Lazy<>(() -> Optional.of(get()).filter(predicate));
}
优化永无止境。
咱们也能够暴露一个方便的工厂方法:
public static <T> Lazy<T> of(Supplier<T> supplier) {
return new Lazy<>(supplier);
}
实际使用上:
Lazy.of(() -> compute(42))
.map(s -> compute(13))
.flatMap(s -> lazyCompute(15))
.filter(v -> v > 0);
你能够看到,只要做为调用链底层的 #get 方法没有被调用,那么什么计算也不会触发。
Null 的处理
某些状况下,null 会被当作有意义的值。不过它与咱们的实现有冲突 —— 一个有意义的 null 值被当作一个未初始化的值,这不太合适。
解决方法也很简单,直接把这种可能的结果包装到一个 Optional 实例里返回。
除此以外,明确禁止 null 做为返回值也是一个好办法,好比:
value = Objects.requireNonNull(supplier.get());
回收再也不使用的 Supplier
有些读者可能已经注意到了,结果计算完毕以后,supplier 就再也不使用了,可是它仍然占据一些资源。
解决办法就是把 Supplier 标记为非 final 的,一旦结果计算完毕,就把它置为 null。
完整的例子
public class Lazy<T> {
private transient Supplier<T> supplier;
private volatile T value;
public Lazy(Supplier<T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
public T get() {
if (value == null) {
synchronized (this) {
if (value == null) {
value = Objects.requireNonNull(supplier.get());
supplier = null;
}
}
}
return value;
}
public <R> Lazy<R> map(Function<T, R> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()));
}
public <R> Lazy<R> flatMap(Function<T, Lazy<R>> mapper) {
return new Lazy<>(() -> mapper.apply(this.get()).get());
}
public Lazy<Optional<T>> filter(Predicate<T> predicate) {
return new Lazy<>(() -> Optional.of(get()).filter(predicate));
}
public static <T> Lazy<T> of(Supplier<T> supplier) {
return new Lazy<>(supplier);
}
}
欢迎工做一到五年的Java工程师朋友们加入Java架构开发:点击连接加入群聊【Java架构交流群】:https://jq.qq.com/?_wv=1027&k=5DmXtYD
本群提供免费的学习指导 架构资料 以及免费的解答
不懂得问题均可以在本群提出来 以后还会有职业生涯规划以及面试指导
同时你们能够多多关注一下小编 你们一块儿学习进步