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

2018 年 8 月 23 日 ImportNew

(点击上方公众号,可快速关注)


来源:ImportNew - yizhe


Java 中惰性求值的潜能,完全被忽视了(在语言层面上,它仅被用来实现 短路求值 )。更先进的语言,如 Scala,区分了传值调用与传名调用,或者引入了 lazy 这样的关键字。


尽管 Java 8 通过延迟队列的实现(java.util.stream.Stream)在惰性求值的方面有些改进,但是我们会先跳过 Stream,而把重点放在如何使用 lambda 表达式实现一个轻量级的惰性求值。


基于 lambda 的惰性求值


Scala


当我们想对 Scala 中的方法参数进行惰性求值时,我们用“传名调用”来实现。


让我们创建一个简单的 foo 方法,它接受一个 String 示例,然后返回这个 String:


def foo(b: String): String = b


一切都是马上返回的,跟 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);

    }

}


以上的代码也可以在 GitHub 上找到。


https://github.com/pivovarit/articles/tree/master/java-lazy-initialization


关于投稿】


如果大家有原创好文投稿,请直接给公号发送留言。


① 留言格式:
【投稿】+《 文章标题》+ 文章链接

② 示例:
【投稿】《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/

③ 最后请附上您的个人简介哈~



看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

登录查看更多
0

相关内容

【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
58+阅读 · 2020年6月26日
【ACL2020】利用模拟退火实现无监督复述
专知会员服务
14+阅读 · 2020年5月26日
【SIGMOD2020-腾讯】Web规模本体可扩展构建
专知会员服务
30+阅读 · 2020年4月12日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
96+阅读 · 2019年12月4日
已删除
将门创投
4+阅读 · 2020年1月6日
后渗透利用msf关闭防火墙
黑白之道
8+阅读 · 2019年8月24日
你必须知道的六个深度炼丹好习惯
GAN生成式对抗网络
9+阅读 · 2019年5月16日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
.NET Core 环境下构建强大且易用的规则引擎
利用 TensorFlow 实现排序和搜索算法
机器学习研究会
5+阅读 · 2017年11月23日
Spark的误解-不仅Spark是内存计算,Hadoop也是内存计算
Feature Selection Library (MATLAB Toolbox)
Arxiv
7+阅读 · 2018年8月6日
Arxiv
5+阅读 · 2015年9月14日
VIP会员
相关资讯
已删除
将门创投
4+阅读 · 2020年1月6日
后渗透利用msf关闭防火墙
黑白之道
8+阅读 · 2019年8月24日
你必须知道的六个深度炼丹好习惯
GAN生成式对抗网络
9+阅读 · 2019年5月16日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
.NET Core 环境下构建强大且易用的规则引擎
利用 TensorFlow 实现排序和搜索算法
机器学习研究会
5+阅读 · 2017年11月23日
Spark的误解-不仅Spark是内存计算,Hadoop也是内存计算
Top
微信扫码咨询专知VIP会员