简单探索 Java 中的惰性计算

前言

惰性计算(尽可能延迟表达式求值)是许多函数式编程语言的特性。惰性集合在需要时提供其元素,无需预先计算它们,这带来了一些好处。

首先,您可以将耗时的计算推迟到绝对需要的时候。其次,您可以创造无限个集合,只要它们继续收到请求,就会继续提供元素。第三,map 和 filter 等函数的惰性使用让您能够得到更高效的代码(请参阅 参考资料 中的链接,加入由 Brian Goetz 组织的相关讨论)。Java 并没有为惰性提供原生支持,但一些框架和后继语言支持这种惰性。

假定使用此伪代码片段来打印列表的长度:

print length([2+1, 3*2, 1/0, 5-4])

如果您尝试执行此代码,结果会因为代码的编程语言类型的不同而有所不同:严格或不严格(也被称为惰性)。在严格的编程语言中,执行(或编译)此代码产生一个 DivByZero 异常,原因是列表的第三个元素。在不严格的语言中,其结果是 4,它准确地报告了列表中的项目数。

毕竟,我调用的方法是 length(),而不是 lengthAndThrowExceptionWhenDivByZero()!Haskell 是为数不多的仍在使用的不严格语言。可惜的是,Java 不支持不严格的计算,但您仍然可以在 Java 中使用惰性的概念。

在 Java 中的惰性迭代器

Java 缺乏对惰性集合的原生支持,但这并不意味着您不能使用 Iterator 模拟一个惰性集合。在本系列的前几篇文章中,我使用了一个简单的素数算法来说明函数式概念。我会在 上期文章 中介绍的优化类的基础上展开本文的讨论,同时提供清单 1 中展示的增强:

清单 1. 确定素数的简单算法

import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.sqrt;
public class Prime {
public static boolean isFactor(int potential, int number) {
return number % potential == 0;
}
public static Set<Integer> getFactors(int number) {
Set<Integer> factors = new HashSet<Integer>();
factors.add(1);
factors.add(number);
for (int i = 2; i < sqrt(number) + 1; i++)
if (isFactor(i, number)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
public static int sumFactors(int number) {
int sum = 0;
for (int i : getFactors(number))
sum += i;
return sum;
}
public static boolean isPrime(int number) {
return number == 2 || sumFactors(number) == number + 1;
}
public static Integer nextPrimeFrom(int lastPrime) {
lastPrime++;
while (! isPrime(lastPrime)) lastPrime++;
return lastPrime;
}
}

前面的一期文章 详细讨论了这个类是如何确定某个整数是否是素数的细节。在 清单 1 中,我添加了 nextPrimeFrom() 方法,根据输入的参数生成下一个素数。该方法在本文即将出现的示例中发挥了重要的作用。

一般情况下,开发人员认为迭代器会使用集合作为后备存储,但是支持 Iterator 接口的任何集合都符合这个条件。因此,我可以创建一个素数的无限迭代器,如清单 2 所示:

清单 2. 创建一个惰性迭代器

public class PrimeIterator implements Iterator<Integer> {
private int lastPrime = 1;
public boolean hasNext() {
return true;
}
public Integer next() {
return lastPrime = Prime.nextPrimeFrom(lastPrime);
}
public void remove() {
throw new RuntimeException("Can't change the fundamental nature of the universe!");
}
}

在 清单 2 中,hasNext() 方法始终返回 true,因为就我们目前所掌握的知识,素数的数量是无限的。remove() 方法在此处不适用,所以在意外调用情况下,会抛出一个异常。沉稳的做法是使用 next() 方法,它用一行代码处理两件事。第一,它调用我在 清单 1 中添加的 nextPrimeFrom() 方法,根据上一个素数生成下一个素数。第二,它利用了 Java 在单个语句中完成赋值与返回结果的能力,更新内部的 lastPrime 字段。我在清单 3 中执行惰性迭代器:

清单 3. 测试惰性迭代器

public class PrimeTest {
private ArrayList<Integer> PRIMES_BELOW_50 = new ArrayList<Integer>() {{
add(2); add(3); add(5); add(7); add(11); add(13);
add(17); add(19); add(23); add(29); add(31); add(37);
add(41); add(43); add(47);
}};
@Test
public void prime_iterator() {
Iterator<Integer> it = new PrimeIterator();
for (int i : PRIMES_BELOW_50) {
assertTrue(i == it.next());
}
}
}

在 清单 3中,我创建了一个 PrimeIterator,并验证它会报告前 50 个素数。虽然这不是迭代器的典型用法,但它模仿一些惰性集合的有用行为。

使用 LazyList

Jakarta Commons 包括一个 LazyList 类,它结合使用了装饰设计模式和工厂。如果要使用 Commons LazyList,则必须包装一个现有列表,使其具有惰性,并为新值创建一个工厂。请考虑使用清单 4 中的 LazyList:

清单 4. 测试 Commons LazyList

public class PrimeTest {
private ArrayList<Integer> PRIMES_BELOW_50 = new ArrayList<Integer>() {{
add(2); add(3); add(5); add(7); add(11); add(13);
add(17); add(19); add(23); add(29); add(31); add(37);
add(41); add(43); add(47);
}};
@Test
public void prime_factory() {
List<Integer> primes = new ArrayList<Integer>();
List<Integer> lazyPrimes = LazyList.decorate(primes, new PrimeFactory());
for (int i = 0; i < PRIMES_BELOW_50.size(); i++)
assertEquals(PRIMES_BELOW_50.get(i), lazyPrimes.get(i));
}
}

在 清单 4 中,我创建一个新的空白 ArrayList 并将它包装在 Commons LazyList.decorate() 方法中,还有一个PrimeFactory 用于生成新值。Commons LazyList 使用列表中已存在的值,不论该值是什么,当对一个尚未赋值的索引调用 get() 方法时,LazyList 会使用工厂(在本例中为 PrimeFactory())生成和填充值。PrimeFactory 出现在清单 5 中:

清单 5. LazyList 使用的 PrimeFactory

public class PrimeFactory implements Factory {
private int index = 0;
@Override
public Object create() {
return Prime.indexedPrime(index++);
}
}

所有惰性列表都需要使用某种方法来生成后续的值。在 清单 2 中,我使用了 next() 方法和 Prime 的 nextPrimeFrom() 方法的组合。对于 清单 4 中的 Commons LazyList,我使用了 PrimeFactory 实例。

Commons LazyList 实现有一个特点,就是在请求新值时,没有信息传递给工厂方法。根据设计,它甚至没有传递所请求元素的索引,强制在 PrimeFactory 类上维护当前状态。这产生了对返回列表的不必要的依赖(因为它必须初始化为空,以便让索引和 PrimeFactory 的内部状态保持同步)。Commons LazyList 是最好的基础实现,但还有更好的开源替代方案,如 Totally Lazy。

Totally Lazy

Totally Lazy 是一个框架,它将一流的惰性添加到 Java。在 前面的一期文章 中,我介绍过 Totally Lazy,但介绍得并不详细。该框架的目标之一是使用静态导入集合来创建一个高度可读的 Java 代码。清单 6 中简单的素数查找程序在编写时充分利用了这个 Totally Lazy 特性:

清单 6. Totally Lazy,充分利用静态导入

import com.googlecode.totallylazy.Predicate;
import com.googlecode.totallylazy.Sequence;
import static com.googlecode.totallylazy.Predicates.is;
import static com.googlecode.totallylazy.numbers.Numbers.equalTo;
import static com.googlecode.totallylazy.numbers.Numbers.increment;
import static com.googlecode.totallylazy.numbers.Numbers.range;
import static com.googlecode.totallylazy.numbers.Numbers.remainder;
import static com.googlecode.totallylazy.numbers.Numbers.sum;
import static com.googlecode.totallylazy.numbers.Numbers.zero;
import static com.googlecode.totallylazy.predicates.WherePredicate.where;
public class Prime {
public static Predicate<Number> isFactor(Number n) {
return where(remainder(n), is(zero));
}
public static Sequence<Number> factors(Number n){
return range(1, n).filter(isFactor(n));
}
public static Number sumFactors(Number n){
return factors(n).reduce(sum);
}
public static boolean isPrime(Number n){
return equalTo(increment(n), sumFactors(n));
}
}

在 清单 6 中,在完成静态导入后,代码是非典型的 Java,但有很强的可读性。Totally Lazy 的部分灵感来自 JUnit 的 Hamcrest 测试扩展的接口。它还使用了 Hamcrest 的一些类。isFactor() 方法变成了对 where() 的调用,结合使用了 Totally Lazy 的 remainder() 与 Hamcrest is() 方法。

同样,factors() 方法变成了针对 range() 对象调用 filter(),我使用现已熟悉的 reduce() 方法来确定总和。最后,isPrime() 方法使用 Hamcrest 的 equalTo() 方法来确定因数的总和是否等于递增的数字。

细心的读者会注意到,清单 6 中的实现的确优化了我在前面的一期文章 中所提及的实现,使用了更高效的算法来确定因数。优化后的版本如清单 7 所示:

清单 7. 优化的素数查找程序的 Totally Lazy 实现

public class PrimeFast {
public static Predicate<Number> isFactor(Number n) {
return where(remainder(n), is(zero));
}
public static Sequence<Number> getFactors(final Number n){
Sequence<Number> lowerRange = range(1, squareRoot(n)).filter(isFactor(n));
return lowerRange.join(lowerRange.map(divide().apply(n)));
}
public static Sequence<Number> factors(final Number n) {
return getFactors(n).memorise();
}
public static Number sumFactors(Number n){
return factors(n).reduce(sum);
}
public static boolean isPrime(Number n){
return equalTo(increment(n), sumFactors(n));
}
}

清单 7 中有两个主要变化。首先,我改进了 getFactors() 算法,以获得平方根下的因数,然后生成平方根之上的对称因数。在 Totally Lazy 中,即使像 divide() 这样的操作也可以使用连贯接口样式来表达。第二个变化涉及内存,它会自动缓存使用相同参数的函数调用,我已经修改了 sumFactors() 方法,以便使用 getFactors() 方法,它是内存化的 getFactors() 方法。Totally Lazy 将内存实现为框架的一部分,因此,实现此优化并不需要更多的代码,但框架的作者将其拼写为 memorise(),而不是更传统的(如在 Groovy 中)memoize()。

像 Totally Lazy 这个名称所声明的那样,Totally Lazy 试图​​在整个框架中尽可能使用惰性。事实上,Totally Lazy 框架本身就包含了一个 primes() 生成程序,它使用框架的构建块实现素数的无限序列。请考虑 Numbers 类的节选代码,如清单 8 所示:

清单 8. 实现无限素数的 Totally Lazy 节选代码

public static Function1<Number, Number> nextPrime = new Function1<Number, Number>() {
@Override
public Number call(Number number) throws Exception {
return nextPrime(number);
}
};
public static Computation<Number> primes = computation(2, computation(3, nextPrime));
public static Sequence<Number> primes() {
return primes;
}
public static LogicalPredicate<Number> prime = new LogicalPredicate<Number>() {
public final boolean matches(final Number candidate) {
return isPrime(candidate);
}
};
public static Number nextPrime(Number number) {
return iterate(add(2), number).filter(prime).second();
}

nextPrime() 方法创建了一个新的 Function1,它是一个伪高阶函数的 Totally Lazy 实现,该方法旨在接受单一 Number 参数,并产生一个 Number 结果。在本例中,它返回 nextPrime() 方法的结果。primes 变量已创建,用于存储素数的状态,使用 2(第一个素数)作为种子值执行计算,并使用一个新的计算来产生下一个素数。这是惰性实现中的典型模式:保存下一个元素,加上用于产生随后的值的方法。prime() 方法仅仅是一个包装程序,围绕之前执行的 prime 计算。

为了确定 清单 8 中的 nextPrime(),Totally Lazy 创建了一个新的 LogicalPredicate,以封装已确定的素数,然后创建 nextPrime() 方法,它在 Totally Lazy 中使用良好的接口来确定下一个素数。

在 Java 中使用低层的静态导入,以促进可读代码的使用,Totally Lazy 在这方面表现得很出色。许多开发人员认为 Java 对于内部的域特定语言是较差的主机,但 Totally Lazy 消除了这种态度。它积极地采用惰性,延缓所有可能的操作。

结束语

在这一期文章中,我探索了惰性计算,首先在 Java 中使用迭代器创建一个模拟惰性集合,然后使用了来自 Jakarta Commons Collections 的基本 LazyList 类。最后,我利用了 Totally Lazy 来实现示例代码,在内部与素数的惰性无限集合中,使用惰性集合来确定素数。Totally Lazy 也说明了良好接口表示,并使用静态导入来提高代码的可读性。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java正则之贪婪匹配、惰性匹配

    贪婪量词: 先看整个字符串是不是一个匹配.如果没有发现匹配,它去掉最后字符串中的最后一个字符,并再次尝试.如果还是没有发现匹配,那么    再次去掉最后一个字符串,这个过程会一直重复直到发现一个匹配或者字符串不剩任何字符.简单量词都是贪婪量词. 惰性量词: 先看字符串中的第一个字母是不是一个匹配,如果单独着一个字符还不够,就读入下一个字符,组成两个字符的字符串.如果还没有发现匹配,惰性量词继续从字符串中添加字符直到发现一个匹配或者整个字符串都检查过也没有匹配.惰性量词和贪婪量词的工作方式恰好相反

  • 简单探索 Java 中的惰性计算

    前言 惰性计算(尽可能延迟表达式求值)是许多函数式编程语言的特性.惰性集合在需要时提供其元素,无需预先计算它们,这带来了一些好处. 首先,您可以将耗时的计算推迟到绝对需要的时候.其次,您可以创造无限个集合,只要它们继续收到请求,就会继续提供元素.第三,map 和 filter 等函数的惰性使用让您能够得到更高效的代码(请参阅 参考资料 中的链接,加入由 Brian Goetz 组织的相关讨论).Java 并没有为惰性提供原生支持,但一些框架和后继语言支持这种惰性. 假定使用此伪代码片段来打印列表

  • java中的数学计算函数的总结

    java中的数学计算函数 Math类: java.lang.Math类中包含基本的数字操作,如指数.对数.平方根和三角函数. java.math是一个包,提供用于执行任意精度整数(BigInteger)算法和任意精度小数(BigDecimal)算法的类. java.lang.Math类中包含E和PI两个静态常量,以及进行科学计算的类(static)方法,可以直接通过类名调用. public static final Double E = 2.7182818284590452354 public

  • 简单谈谈Java中的方法和方法重载

    今天我们就讲一点内容,来说说Java中的方法和方法重载以及需要注意的一些地方: 方法: Java的方法类似与其他语言的函数,是一段用来完成特定功能的代码片段, 声明格式: [修饰符1 修饰符2 ....] ,返回值类型 方法名 (形式参数列表) { Java语句: - - -} 形式参数:在方法被调用时用于接受外界输入的数据: 实参: 调用方法时世界传给方法的数据: 返回值: 方法在执行完毕后返回给调用他的环境的数据: 返回值类型: 事先约定好的返回值的数据类型,如无返回值必须给出返回值类型vo

  • 简单了解java中静态初始化块的执行顺序

    这篇文章主要介绍了简单了解java中静态初始化块的执行顺序,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在java中,其应该是先于所有的方法执行. 下面是测试代码: public class Test1 { static{ System.out.println("执行静态初始化块test1..."); } { System.out.println("执行初始化块test1"); } public Test1(){

  • 简单了解java中int和Integer的区别

    这篇文章主要介绍了简单了解java中int和Integer的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.Integer是int的包装类,int则是java的一种基本数据类型 2.Integer变量必须实例化(new 一下是最常见的实例化)后才能使用,而int变量不需要 3.Integer实际是对象的引用,new Integer(),实际上是生成一个指针指向此对象:而int则是直接存储数据值 4.Integer的默认值是null,in

  • 简单了解Java中的可重入锁

    这篇文章主要介绍了简单了解Java中的可重入锁,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文里面讲的是广义上的可重入锁,而不是单指JAVA下的ReentrantLock. 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响. 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁. 下面是使用实例: package reentrantLock; pu

  • 简单讲解java中throws与throw的区别

    Java中throws和throw的区别讲解 当然,你需要明白异常在Java中式以一个对象来看待. 并且所有系统定义的编译和运行异常都可以由系统自动抛出,称为标准异常,但是一般情况下Java 强烈地要求应用程序进行完整的异常处理,给用户友好的提示,或者修正后使程序继续执行. 直接进入正题哈: 1.用户程序自定义的异常和应用程序特定的异常,必须借助于 throws 和 throw 语句来定义抛出异常. 1.1   throw是语句抛出一个异常. 语法:throw (异常对象);         

  • 简单介绍java中equals以及==的用法

    简单介绍 equals方法是java.lang.Object类的方法有两种用法说明: 一.对于字符串变量来说,使用"=="和"equals()"方法比较字符串时,其比较方法不同. 1."=="比较两个变量本身的值,即两个对象在内存中的首地址.(java中,对象的首地址是它在内存中存放的起始地址,它后面的地址是用来存放它所包含的各个属性的地址,所以内存中会用多个内存块来存放对象的各个参数,而通过这个首地址就可以找到该对象,进而可以找到该对象的各个属

  • Java中System.currentTimeMillis()计算方式与时间单位转换讲解

    一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s) 1秒=1,000,000 微秒(μs) 1微秒=1/1,000,000秒(s) 1秒=1,000,000,000 纳秒(ns) 1纳秒=1/1,000,000,000秒(s) 1秒=1,000,000,000,000 皮秒(ps) 1皮秒=1/1,000,000,000,000秒(s) 1分钟=60秒 1小时=60分钟=3600秒 二.System.currentTimeMillis()计算方式 在开发过程中,通常很

  • 探索Java中private方法添加@Transactional事务未生效原因

    现在产品期望用户创建和保存逻辑分离:把User实例的创建和保存逻辑拆到两个方法分别进行.然后,把事务的注解 @Transactional 加在保存数据库的方法上. @Service public class StudentService { @Autowired private StudentMapper studentMapper; @Autowired private StudentService studentService; public void saveStudent(String

随机推荐