简单探索 Java 中的惰性计算
前言
惰性计算(尽可能延迟表达式求值)是许多函数式编程语言的特性。惰性集合在需要时提供其元素,无需预先计算它们,这带来了一些好处。
首先,您可以将耗时的计算推迟到绝对需要的时候。其次,您可以创造无限个集合,只要它们继续收到请求,就会继续提供元素。第三,map和filter等函数的惰性使用让您能够得到更高效的代码(请参阅参考资料中的链接,加入由BrianGoetz组织的相关讨论)。Java并没有为惰性提供原生支持,但一些框架和后继语言支持这种惰性。
假定使用此伪代码片段来打印列表的长度:
printlength([2+1,3*2,1/0,5-4])
如果您尝试执行此代码,结果会因为代码的编程语言类型的不同而有所不同:严格或不严格(也被称为惰性)。在严格的编程语言中,执行(或编译)此代码产生一个DivByZero异常,原因是列表的第三个元素。在不严格的语言中,其结果是4,它准确地报告了列表中的项目数。
毕竟,我调用的方法是length(),而不是lengthAndThrowExceptionWhenDivByZero()!Haskell是为数不多的仍在使用的不严格语言。可惜的是,Java不支持不严格的计算,但您仍然可以在Java中使用惰性的概念。
在Java中的惰性迭代器
Java缺乏对惰性集合的原生支持,但这并不意味着您不能使用Iterator模拟一个惰性集合。在本系列的前几篇文章中,我使用了一个简单的素数算法来说明函数式概念。我会在上期文章中介绍的优化类的基础上展开本文的讨论,同时提供清单1中展示的增强:
清单1.确定素数的简单算法
importjava.util.HashSet; importjava.util.Set; importstaticjava.lang.Math.sqrt; publicclassPrime{ publicstaticbooleanisFactor(intpotential,intnumber){ returnnumber%potential==0; } publicstaticSetgetFactors(intnumber){ Set factors=newHashSet (); factors.add(1); factors.add(number); for(inti=2;i 前面的一期文章详细讨论了这个类是如何确定某个整数是否是素数的细节。在清单1中,我添加了nextPrimeFrom()方法,根据输入的参数生成下一个素数。该方法在本文即将出现的示例中发挥了重要的作用。
一般情况下,开发人员认为迭代器会使用集合作为后备存储,但是支持Iterator接口的任何集合都符合这个条件。因此,我可以创建一个素数的无限迭代器,如清单2所示:
清单2.创建一个惰性迭代器
publicclassPrimeIteratorimplementsIterator{ privateintlastPrime=1; publicbooleanhasNext(){ returntrue; } publicIntegernext(){ returnlastPrime=Prime.nextPrimeFrom(lastPrime); } publicvoidremove(){ thrownewRuntimeException("Can'tchangethefundamentalnatureoftheuniverse!"); } } 在清单2中,hasNext()方法始终返回true,因为就我们目前所掌握的知识,素数的数量是无限的。remove()方法在此处不适用,所以在意外调用情况下,会抛出一个异常。沉稳的做法是使用next()方法,它用一行代码处理两件事。第一,它调用我在清单1中添加的nextPrimeFrom()方法,根据上一个素数生成下一个素数。第二,它利用了Java在单个语句中完成赋值与返回结果的能力,更新内部的lastPrime字段。我在清单3中执行惰性迭代器:
清单3.测试惰性迭代器
publicclassPrimeTest{ privateArrayListPRIMES_BELOW_50=newArrayList (){{ 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 publicvoidprime_iterator(){ Iterator it=newPrimeIterator(); for(inti:PRIMES_BELOW_50){ assertTrue(i==it.next()); } } } 在清单3中,我创建了一个PrimeIterator,并验证它会报告前50个素数。虽然这不是迭代器的典型用法,但它模仿一些惰性集合的有用行为。
使用LazyList
JakartaCommons包括一个LazyList类,它结合使用了装饰设计模式和工厂。如果要使用CommonsLazyList,则必须包装一个现有列表,使其具有惰性,并为新值创建一个工厂。请考虑使用清单4中的LazyList:
清单4.测试CommonsLazyList
publicclassPrimeTest{ privateArrayListPRIMES_BELOW_50=newArrayList (){{ 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 publicvoidprime_factory(){ List primes=newArrayList (); List lazyPrimes=LazyList.decorate(primes,newPrimeFactory()); for(inti=0;i 在清单4中,我创建一个新的空白ArrayList并将它包装在CommonsLazyList.decorate()方法中,还有一个PrimeFactory用于生成新值。CommonsLazyList使用列表中已存在的值,不论该值是什么,当对一个尚未赋值的索引调用get()方法时,LazyList会使用工厂(在本例中为PrimeFactory())生成和填充值。PrimeFactory出现在清单5中:
清单5.LazyList使用的PrimeFactory
publicclassPrimeFactoryimplementsFactory{ privateintindex=0; @Override publicObjectcreate(){ returnPrime.indexedPrime(index++); } }所有惰性列表都需要使用某种方法来生成后续的值。在清单2中,我使用了next()方法和Prime的nextPrimeFrom()方法的组合。对于清单4中的CommonsLazyList,我使用了PrimeFactory实例。
CommonsLazyList实现有一个特点,就是在请求新值时,没有信息传递给工厂方法。根据设计,它甚至没有传递所请求元素的索引,强制在PrimeFactory类上维护当前状态。这产生了对返回列表的不必要的依赖(因为它必须初始化为空,以便让索引和PrimeFactory的内部状态保持同步)。CommonsLazyList是最好的基础实现,但还有更好的开源替代方案,如TotallyLazy。
TotallyLazy
TotallyLazy是一个框架,它将一流的惰性添加到Java。在前面的一期文章中,我介绍过TotallyLazy,但介绍得并不详细。该框架的目标之一是使用静态导入集合来创建一个高度可读的Java代码。清单6中简单的素数查找程序在编写时充分利用了这个TotallyLazy特性:
清单6.TotallyLazy,充分利用静态导入
importcom.googlecode.totallylazy.Predicate; importcom.googlecode.totallylazy.Sequence; importstaticcom.googlecode.totallylazy.Predicates.is; importstaticcom.googlecode.totallylazy.numbers.Numbers.equalTo; importstaticcom.googlecode.totallylazy.numbers.Numbers.increment; importstaticcom.googlecode.totallylazy.numbers.Numbers.range; importstaticcom.googlecode.totallylazy.numbers.Numbers.remainder; importstaticcom.googlecode.totallylazy.numbers.Numbers.sum; importstaticcom.googlecode.totallylazy.numbers.Numbers.zero; importstaticcom.googlecode.totallylazy.predicates.WherePredicate.where; publicclassPrime{ publicstaticPredicateisFactor(Numbern){ returnwhere(remainder(n),is(zero)); } publicstaticSequence factors(Numbern){ returnrange(1,n).filter(isFactor(n)); } publicstaticNumbersumFactors(Numbern){ returnfactors(n).reduce(sum); } publicstaticbooleanisPrime(Numbern){ returnequalTo(increment(n),sumFactors(n)); } } 在清单6中,在完成静态导入后,代码是非典型的Java,但有很强的可读性。TotallyLazy的部分灵感来自JUnit的Hamcrest测试扩展的接口。它还使用了Hamcrest的一些类。isFactor()方法变成了对where()的调用,结合使用了TotallyLazy的remainder()与Hamcrestis()方法。
同样,factors()方法变成了针对range()对象调用filter(),我使用现已熟悉的reduce()方法来确定总和。最后,isPrime()方法使用Hamcrest的equalTo()方法来确定因数的总和是否等于递增的数字。
细心的读者会注意到,清单6中的实现的确优化了我在前面的一期文章中所提及的实现,使用了更高效的算法来确定因数。优化后的版本如清单7所示:
清单7.优化的素数查找程序的TotallyLazy实现
publicclassPrimeFast{ publicstaticPredicateisFactor(Numbern){ returnwhere(remainder(n),is(zero)); } publicstaticSequence getFactors(finalNumbern){ Sequence lowerRange=range(1,squareRoot(n)).filter(isFactor(n)); returnlowerRange.join(lowerRange.map(divide().apply(n))); } publicstaticSequence factors(finalNumbern){ returngetFactors(n).memorise(); } publicstaticNumbersumFactors(Numbern){ returnfactors(n).reduce(sum); } publicstaticbooleanisPrime(Numbern){ returnequalTo(increment(n),sumFactors(n)); } } 清单7中有两个主要变化。首先,我改进了getFactors()算法,以获得平方根下的因数,然后生成平方根之上的对称因数。在TotallyLazy中,即使像divide()这样的操作也可以使用连贯接口样式来表达。第二个变化涉及内存,它会自动缓存使用相同参数的函数调用,我已经修改了sumFactors()方法,以便使用getFactors()方法,它是内存化的getFactors()方法。TotallyLazy将内存实现为框架的一部分,因此,实现此优化并不需要更多的代码,但框架的作者将其拼写为memorise(),而不是更传统的(如在Groovy中)memoize()。
像TotallyLazy这个名称所声明的那样,TotallyLazy试图在整个框架中尽可能使用惰性。事实上,TotallyLazy框架本身就包含了一个primes()生成程序,它使用框架的构建块实现素数的无限序列。请考虑Numbers类的节选代码,如清单8所示:
清单8.实现无限素数的TotallyLazy节选代码
publicstaticFunction1nextPrime=newFunction1 (){ @Override publicNumbercall(Numbernumber)throwsException{ returnnextPrime(number); } }; publicstaticComputation primes=computation(2,computation(3,nextPrime)); publicstaticSequence primes(){ returnprimes; } publicstaticLogicalPredicate prime=newLogicalPredicate (){ publicfinalbooleanmatches(finalNumbercandidate){ returnisPrime(candidate); } }; publicstaticNumbernextPrime(Numbernumber){ returniterate(add(2),number).filter(prime).second(); } nextPrime()方法创建了一个新的Function1,它是一个伪高阶函数的TotallyLazy实现,该方法旨在接受单一Number参数,并产生一个Number结果。在本例中,它返回nextPrime()方法的结果。primes变量已创建,用于存储素数的状态,使用2(第一个素数)作为种子值执行计算,并使用一个新的计算来产生下一个素数。这是惰性实现中的典型模式:保存下一个元素,加上用于产生随后的值的方法。prime()方法仅仅是一个包装程序,围绕之前执行的prime计算。
为了确定清单8中的nextPrime(),TotallyLazy创建了一个新的LogicalPredicate,以封装已确定的素数,然后创建nextPrime()方法,它在TotallyLazy中使用良好的接口来确定下一个素数。
在Java中使用低层的静态导入,以促进可读代码的使用,TotallyLazy在这方面表现得很出色。许多开发人员认为Java对于内部的域特定语言是较差的主机,但TotallyLazy消除了这种态度。它积极地采用惰性,延缓所有可能的操作。
结束语
在这一期文章中,我探索了惰性计算,首先在Java中使用迭代器创建一个模拟惰性集合,然后使用了来自JakartaCommonsCollections的基本LazyList类。最后,我利用了TotallyLazy来实现示例代码,在内部与素数的惰性无限集合中,使用惰性集合来确定素数。TotallyLazy也说明了良好接口表示,并使用静态导入来提高代码的可读性。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。