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

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);

}

}

原文地址:http://blog.51cto.com/13952953/2286634

时间: 2024-11-06 12:25:52

利用 Lambda 表达式实现 Java 中的惰性求值的相关文章

Java 终于有 Lambda 表达式啦~Java 8 语言变化&mdash;&mdash;Lambda 表达式和接口类更改【转载】

原文地址 en cn 下载 Demo Java? 8 包含一些重要的新的语言功能,为您提供了构建程序的更简单方式.Lambda 表达式 为内联代码块定义一种新语法,其灵活性与匿名内部类一样,但样板文件要少得多.接口更改使得接口可以添加到现有接口中,同时又不会破坏与现有代码的兼容性.本文将了解这些更改是如何协同工作的. Java 8 的最大变化在于添加了对 lambda 表达式 的支持.Lambda 表达式是可按引用传递的代码块.类似于一些其他编程语言中的闭包:它们是实现某项功能的代码,可接受一个

Lambda 表达式 in java 8

Lambda 表达式 in Java 8 Lambda表达式是java 8 新增的特性 Lambda表达式主要作用:支持将代码块作为方法参数,允许使用更简洁的代码创建函数式接口的实例,是匿名内部类的一种简化,可以部分取代匿名内部类的作用. 函数式接口:只有一个抽象方法的接口. Lambda表达式又称为匿名函数(anonymous function),表示一类无需定义标识符(函数名)的函数或者子程序,可以看做是一种语法糖. Lambda 表达式语法格式 Lambda表达式在java中由三部分组成:

Swift 之惰性求值

Swift 之惰性求值 在 Haskell 中的函数有一个特性 函数的返回可能是一个块(一个被延迟计算的表达式) 从而引出一个概念 惰性求值 isOdd n = mod n 2 == 1 下面有段 分析 在使用严格求值的语言里,函数的参数总是在应用函数之前被求值.以 isOdd 为例子:子表达式 (1 + 2) 会首先被求值,得出结果 3 .接着,将 3 绑定到变量 n ,应用到函数 isOdd .最后, mod 3 2 返回 1 ,而 1 == 1 返回 True . Haskell 使用了另

惰性求值——lodash源码解读

前言 lodash受欢迎的一个原因,是其优异的计算性能.而其性能能有这么突出的表现,很大部分就来源于其使用的算法--惰性求值. 本文将讲述lodash源码中,惰性求值的原理和实现. 一.惰性求值的原理分析 惰性求值(Lazy Evaluation),又译为惰性计算.懒惰求值,也称为传需求调用(call-by-need),是计算机编程中的一个概念,它的目的是要最小化计算机要做的工作. 惰性求值中的参数直到需要时才会进行计算.这种程序实际上是从末尾开始反向执行的.它会判断自己需要返回什么,并继续向后

关于lambda表达式的一些学习——基于谓词筛选值序列

今天看了一些关于lambda表达式的知识,然后对于Func<T,TResult>泛型委托不太熟悉,便查了查相关资料,又引出来了基于谓词筛选值序列这个对我来说的新鲜知识点,于是去查MSDN,以下是看到的一些相关介绍: 此方法通过使用延迟执行实现. 即时返回值为一个对象,该对象存储执行操作所需的所有信息. 只有通过直接调用对象的 GetEnumerator 方法或使用 Visual C# 中的 foreach(或 Visual Basic 中的 For Each)来枚举该对象时,才执行此方法表示的

C#函数式编程之惰性求值

惰性求值 在开始介绍今天要讲的知识之前,我们想要理解严格求值策略和非严格求值策略之间的区别,这样我们才能够深有体会的明白为什么需要利用这个技术.首先需要说明的是C#语言小部分采用了非严格求值策略,大部分还是严格求值策略.首先我们先演示非严格求值策略的情况,我们先在控制台项目中写一个DoOneThing方法. 然后在Main方法中写入下面这串代码: 然后我们运行程序,会发现DoOneThing方法并没有执行.当然这看起来也很正常,因为这是或,并且第一个已经是true了.整个表达式就是true了,自

Stream01 定义、迭代、操作、惰性求值

1 Stream Stream 是 Java 8 提供的一系列对可迭代元素处理的优化方案,使用 Stream 可以大大减少代码量,提高代码的可读性并且使代码更易并行. 2 迭代 2.1 需求 随机创建int类型的数组,计算数组中各个元素的总和 2.2 思路 2.2.1 外部迭代 通过for循环迭代数组 2.2.2 内部迭代 先将数组转化成流 -> 在通过流的相关操作来实现 2.2.3 外部迭代和内部迭代 外部迭代式串行的,如果要实现并行需要自己编写代码实现:内部迭代实现并行操作只需要调用一个pa

好炫的Lambda表达式,Java党用起来!(最简易Lambda教程)

刷微博时偶然看到Lambda 的 HelloWorld 教程,觉得很酷炫!分享一下! 效果示例 总体看起来效果就是代码简洁,如下 //之前的写法 btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("hello lambda"); ... } }); //Lambda表达式写法 btn.setsetOnClickLis

浅谈利用同步机制解决Java中的线程安全问题

我们知道大多数程序都不会是单线程程序,单线程程序的功能非常有限,我们假设一下所有的程序都是单线程程序,那么会带来怎样的结果呢?假如淘宝是单线程程序,一直都只能一个一个用户去访问,你要在网上买东西还得等着前面千百万人挑选购买,最后心仪的商品下架或者售空......假如饿了吗是单线程程序,那么一个用户得等前面全国千万个用户点完之后才能进行点餐,那饿了吗就该倒闭了不是吗?以上两个简单的例子,就说明一个程序能进行多线程并发访问的重要性,今天就让我们去了解一下Java中多线程并发访问这个方向吧. **第一