继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

利用 Lambda 表达式实现 Java 中的惰性求值

慕神8447489
关注TA
已关注
手记 1259
粉丝 174
获赞 956

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


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP