如果您从事大型企业项目开发您就会熟悉编写模块化代码的好处良构的模块化的代码更容易编写调试理解和重用Java 开发人员的问题是函数编程范型长期以来只是通过像 HaskellSchemeErlang 和 Lisp 这样的特殊语言实现的在本文中作者 Abhijit Belapurkar 展示了如何使用像闭包(closure)和 高阶函数(higher order function)这样的函数编程结构在 Java 语言中编写良构的模块化的代码
Java 语言中常被忽视的一个方面是它被归类为一种命令式(imperative)编程语言命令式编程虽然由于与 Java 语言的关联而相当普及但是并不是惟一可用的编程风格也不总是最有效的在本文中我将探讨在 Java 开发实践中加入不同的编程方法 ── 即函数编程(FP)
命令式编程是一种用程序状态描述计算的方法使用这种范型的编程人员用语句改变程序状态这就是为什么像 Java 这样的程序是由一系列让计算机执行的命令 (或者语句) 所组成的 另一方面 函数编程是一种强调表达式的计算而非命令的执行的一种编程风格表达式是用函数结合基本值构成的它类似于用参数调用函数
本文将介绍函数编程的基本特点但是重点放在两个特别适用于 Java 开发框架的元素闭包和高阶函数如果您曾经使用过像 PythonRuby 或者 Groovy (请参阅 参考资料) 这样的敏捷开发语言那么您就可能已经遇到过这些元素在这里您将看到在 Java 开发框架中直接使用它们会出现什么情况我将首先对函数编程及其核心元素做一个简短的概念性的综述然后用常用的编程场景展示用结构化的方式使用闭包和高阶函数会给 Java 代码带来什么好处
什么是函数编程?
在经常被引用的论文 Why Functional Programming Matters(请参阅 参考资料) 中作者 John Hughes 说明了模块化是成功编程的关键而函数编程可以极大地改进模块化在函数编程中编程人员有一个天然框架用来开发更小的更简单的和更一般化的模块 然后将它们组合在一起函数编程的一些基本特点包括
支持闭包和高阶函数
支持懒惰计算(lazy evaluation)
使用递归作为控制流程的机制
加强了引用透明性
没有副作用
我将重点放在在 Java 语言中使用闭包和高阶函数上但是首先对上面列出的所有特点做一个概述
闭包和高阶函数
函数编程支持函数作为第一类对象有时称为 闭包或者 仿函数(functor)对象实质上闭包是起函数的作用并可以像对象一样操作的对象与此类似FP 语言支持 高阶函数高阶函数可以用另一个函数(间接地用一个表达式) 作为其输入参数在某些情况下它甚至返回一个函数作为其输出参数这两种结构结合在一起使得可以用优雅的方式进行模块化编程这是使用 FP 的最大好处
命令式编程
命令式编程这个名字是从自然语言(比如英语)的 祈使语气(imperative mood)衍生出来的在这种语气中宣布命令并按照执行除 Java 语言之外C 和 C++ 是另外两种广泛使用的符合命令式风格的高级编程语言
懒惰计算
除了高阶函数和仿函数(或闭包)的概念FP 还引入了 懒惰计算的概念在懒惰计算中表达式不是在绑定到变量时立即计算而是在求值程序需要产生表达式的值时进行计算延迟的计算使您可以编写可能潜在地生成无穷输出的函数因为不会计算多于程序的其余部分所需要的值所以不需要担心由无穷计算所导致的 outofmemory 错误一个懒惰计算的例子是生成无穷 Fibonacci 列表的函数但是对 第 n 个Fibonacci 数的计算相当于只是从可能的无穷列表中提取一项
递归
FP 还有一个特点是用递归做为控制流程的机制例如Lisp 处理的列表定义为在头元素后面有子列表这种表示法使得它自己自然地对更小的子列表不断递归
关于实现库
我使用了由 Apache Commons Functor 项目提供的库构建本文使用的例子Apache Commons Functor 库包括大量基本构造可以在涉及闭包和高阶函数的复杂使用场景中重复使用当然可以使用不同的实现(如 Java Generic LibrariesMango 或者 Generic Algorithms for Java)而不会对在本文中所讨论和展示的概念有影响尽管您必须下载和使用 Apache Commons Functor 库才能演示这里的例子
引用透明性
函数程序通常还加强 引用透明性即如果提供同样的输入那么函数总是返回同样的结果就是说表达式的值不依赖于可以改变值的全局状态这使您可以从形式上推断程序行为因为表达式的意义只取决于其子表达式而不是计算顺序或者其他表达式的副作用这有助于验证正确性简化算法甚至有助于找出优化它的方法
副作用
副作用是修改系统状态的语言结构因为 FP 语言不包含任何赋值语句变量值一旦被指派就永远不会改变而且调用函数只会计算出结果 ── 不会出现其他效果因此FP 语言没有副作用
这些基本描述应足以让您完成本文中的函数编程例子有关这个主题的更多参考资料请参阅 参考资料一节
Java 语言中的函数编程
不管是否相信在 Java 开发实践中您可能已经遇到过闭包和高阶函数尽管当时您可能没有意识到例如许多 Java 开发人员在匿名内部类中封闭 Java 代码的一个词汇单元(lexical unit)时第一次遇到了 闭包这个封闭的 Java 代码单元在需要时由一个 高阶函数 执行例如清单 中的代码在一个类型为 javalangRunnable 的对象中封闭了一个方法调用
清单 隐藏的闭包
Runnable worker = new Runnable()
{
public void run()
{
parseData();
}
};
方法 parseData 确实 封闭(因而有了名字 闭包)在 Runnable 对象的实例 worker 中它可以像数据一样在方法之间传递并可以在任何时间通过发送消息(称为 run ) 给 worker 对象而执行
更多的例子
另一个在面向对象世界中使用闭包和高阶函数的例子是 Visitor 模式如果还不熟悉这种模式请参阅 参考资料以了解更多有关它的内容基本上Visitor 模式展现一个称为 Visitor 的参与者该参与者的实例由一个复合对象(或者数据结构)接收并应用到这个数据结构的每一个构成节点Visitor 对象实质上 封闭 了处理节点/元素的逻辑使用数据结构的 accept (visitor) 方法作为应用逻辑的高阶函数
通过使用适当不同的 Visitor 对象(即闭包)可以对数据结构的元素应用完全不同的处理逻辑与此类似可以向不同的高阶函数传递同样的闭包以用另一种方法处理数据结构(例如这个新的高阶函数可以实现不同逻辑用于遍历所有构成元素)
类 javautilsCollections 提供了另一个例子这个类在版本 以后成为了 Java SDK 的一部分它提供的一种实用程序方法是对在 javautilList 中包含的元素排序不过它使调用者可以将排序列表元素的逻辑封装到一个类型为 javautilComparator 的对象中其中 Comparator 对象作为第二个参数传递给排序方法
在内部 sort 方法的引用实现基于 合并排序(mergesort) 算法它通过对顺序中相邻的对象调用 Comparator 对象的 compare 方法遍历列表(list)中的元素在这种情况下 Comparator 是闭包而 Collectionssort 方法是高阶函数这种方式的好处是明显的可以根据在列表中包含的不同对象类型传递不同的 Comparator 对象(因为如何比较列表中任意两个对象的逻辑安全地封装在 Comparator 对象中)与此类似 sort 方法的另一种实现可以使用完全不同的算法(比如说 快速排序(quicksort)) 并仍然重复使用同样的 Comparator 对象将它作为基本函数应用到列表中两个元素的某种组合
创建闭包
广意地说有两种生成闭包的技术使用闭包的代码可以等效地使用这两种技术创建闭包后可以以统一的方式传递它也可以向它发送消息以让它执行其封装的逻辑因此技术的选择是偏好的问题在某些情况下也与环境有关
最后一次要求下载!
从这以后的讨论将结合基于 Apache Commons Functor 库的例子如果您还没有下载这个库应当 现在就下载我将假定您可以访问 Javadocs 所带的 Apache 库因此对单独的库类不再做过多的说明
在第一种技术 表达式特化(expression specialization)中由基础设施为闭包提供一个一般性的接口通过编写这个接口的特定实现创建具体的闭包在第二种技术 表达式合成(expression composition) 中基础设施提供实现了基本一元/二元/三元//n 元操作(比如一元操作符 not 和二元操作符 and / or )的具体帮助类在这里新的闭包由这些基本构建块的任意组合创建而成
我将在下面的几节中详细讨论这两种技术
表达式特化
假定您在编写一个在线商店的应用程序商店中可提供的商品用类 SETLItem 表示每一件商品都有相关的标签价格 SETLItem 类提供了名为 getPrice 的方法对商品实例调用这个方法时会返回该商品的标签价格
如何检查 item 的成本是否不低于 item 呢?在 Java 语言中一般要编写一个像这样的表达式
assert(itemgetPrice() >= itemgetPrice());
像这样的表达式称为 二元谓词(binary predicate) 二元是因为它取两个参数而 谓词 是因为它用这两个参数做一些事情并生成布尔结果不过要注意只能在执行流程中执行上面的表达式它的输出取决于 item 和 item 在特定瞬间的值从函数编程的角度看这个表达式还不是一般性的逻辑就是说它不能不管执行控制的当前位置而随心所欲地传递并执行
为了使二元谓词发挥作用必须将它封装到一个对象中通过 特化(specializing) 一个称为 BinaryPredicate 的接口做到这一点这个接口是由 Apache Functor 库提供的如清单 所示
清单 表达式特化方法
package syssetlfp;
public class SETLItem
{
private String name;
private String code;
private int price;
private String category;
public int getPrice()
{
return price;
}
public void setPrice(int inPrice)
{
price = inPrice;
}
public String getName()
{
return name;
}
public void setName(String inName)
{
name = inName;
}
public String getCode()
{
return code;
}
public void setCode(String inCode)
{
code = inCode;
}
public String getCategory()
{
return category;
}
public void setCategory(String inCategory)
{
category = inCategory;
}
}
package syssetlfp;
import javautilComparator;
public class PriceComparator implements Comparator
{
public int compare (Object o Object o)
{
return (((SETLItem)o)getPrice()((SETLItem)o)getPrice());
}
}
package syssetlfp;
import monsfunctor*;
import parator*;
import javautil*;
public class TestA
{
public static void main(String[] args)
{
try
{
Comparator pc = new PriceComparator();
BinaryPredicate bp = new IsGreaterThanOrEqual(pc);
SETLItem item = new SETLItem();
itemsetPrice();
SETLItem item = new SETLItem();
itemsetPrice();
if (bptest(item item))
Systemoutprintln(Item costs more than Item!);
else
Systemoutprintln(Item costs more than Item!);
SETLItem item = new SETLItem();
itemsetPrice();
if (bptest(item item))
Systemoutprintln(Item costs more than Item!);
else
Systemoutprintln(Item costs more than Item!);
}
catch (Exception e)
{
eprintStackTrace();
}
}
}
BinaryPredicate 接口以由 Apache Functor 库提供的 IsGreaterThanOrEqual 类的形式特化 PriceComparator 类实现了 javautilComparator 接口并被作为输入传递给 IsGreaterThanOrEqual 类收到一个 test 消息时 IsGreaterThanOrEqual 类自动调用 PriceComparator 类的 compare 方法 compare 方法预期接收两个 SETLItem 对象相应地它返回两个商品的价格差 compare 方法返回的正值表明 item 的成本不低于 item
初看之下对一个相当基本的操作要做很多的工作那它有什么好处呢?特化 BinaryPredicate 接口(而不是编写 Java 比较表达式) 使您无论在何时何地都可以比较任意两个商品的价格可以将 bp 对象作为数据传递并向它发送消息以在任何时候使用这两个参数的任何值来执行它(称为 test )
表达式合成
表达式合成是得到同样结果的一种稍有不同的技术考虑计算特定 SETLItem 的净价问题要考虑当前折扣和销售税率清单 列出了这个问题基于仿函数的解决方式
清单 表达式合成方法
package syssetlfp;
import monsfunctorBinaryFunction;
import monsfunctorUnaryFunction;
import monsfunctoradapterLeftBoundFunction;
public class Multiply implements BinaryFunction
{
public Object evaluate(Object left Object right)
{
return new Double(((Double)left)doubleValue() * ((Double)right)doubleValue());
}
}
package syssetlfp;
import monsfunctor*;
import posite*;
import monsfunctoradapter*;
import monsfunctorUnaryFunction;
public class TestB
{
public static void main(String[] args)
{
try
{
double discountRate = ;
double taxRate=;
SETLItem item = new SETLItem();
itemsetPrice();
UnaryFunction calcDiscountedPrice =
new RightBoundFunction(new Multiply() new Double(discountRate));
UnaryFunction calcTax =
new RightBoundFunction(new Multiply() new Double(+taxRate));
CompositeUnaryFunction calcNetPrice =
new CompositeUnaryFunction(calcTax calcDiscountedPrice);
Double netPrice = (Double)calcNetPriceevaluate(new Double(itemgetPrice()));
Systemoutprintln(The net price is: + netPrice);
}
catch (Exception e)
{
eprintStackTrace();
}
}
}
BinaryFunction 类似于前面看到的 BinaryPredicate 是一个由 Apache Functor 提供的一般化仿函数(functor)接口 BinaryFunction 接口有两个参数并返回一个 Object 值类似地 UnaryFunction 是一个取一个 Object 参数并返回一个 Object 值的仿函数接口
RightBoundFunction 是一个由 Apache 库提供的适配器类它通过使用常量右参数(rightside argument)将 BinaryFunction 适配给 UnaryFunction 接口即在一个参数中收到相应的消息( evaluate ) 时它在内部用两个参数发送一个 evaluate 消息给正在适配的 BinaryFunction ── 左边的是发送给它的参数右边的是它知道的常量您一定会猜到名字 RightBoundFunction 来自于常量值是作为第二个 (右边) 参数传递这一事实(是的Apache 库还提供了一个 LeftBoundFunction 其中常量是作为第一个参数或者左参数传递的)
用于双精度相乘的特化的 BinaryFunction
清单 显示了名为 Multiply 的特化的 BinaryFunction 它取两个 Double 作为输入并返回一个新的由前两个双精度值相乘而得到 Double
在 calcDiscountedRate 中实例化了一个新的 RightBoundFunction 它通过用 ( discountRate) 作为其常量第二参数将二元 Multiply 函数适配为一元接口
结果可以用一个 Double 参数向 calcDiscountRate 发送一个名为 evaluate 的消息在内部输入参数 Double 乘以 calcDiscountRate 对象本身包含的常量值
与此类似在 calcTaxRate 中实例化了一个新的 RightBoundFunction 它通过用 ( + taxRate) 作为其第二个常量参数将二元 Multiply 函数适配为一元接口结果可以用一个 Double 参数向 calcTaxRate 发送一个名为 evaluate 的消息在内部输入参数 Double 乘以 calcTaxRate 对象本身包含的常量值
这种将多参数的函数重新编写为一个参数的函数的合成(composition)技术也称为 currying
合成魔术在最后的时候就发挥作用了实质上计算对象净价的算法是首先计算折扣价格(使用 calcDiscountRate 仿函数)然后通过在上面加上销售税(用 calcSalesTax 仿函数)计算净价就是说需要组成一个函数在内部调用第一个仿函数并将计算的输出流作为第二个仿函数的计算的输入Apache 库提供了用于这种目的的一个内置仿函数称为 CompositeUnaryFunction
在清单 中 CompositeUnaryFunction 实例化为变量 calcNetPrice 作为 calcDiscountRate 和 calcSalesTax 仿函数的合成与前面一样将可以向其他函数传递这个对象其他函数也可以通过向它发送一个包含商品参数的 evaluate 消息要求它计算这种商品的净价
一元与二元合成
在清单 中您看到了 一元合成的一个例子其中一个一元仿函数的结果是另一个的输入另一种合成称为 二元合成作为 evaluate 消息的一部分需要传递两个一元仿函数的结果作为二元仿函数的参数
清单 是说明二元合成的必要性和风格的一个例子假定希望保证商店可以给出的最大折扣有一个最大限度因此必须将作为 calcDiscount 仿函数计算结果得到的折扣量与 cap 值进行比较并取最小值作为计算出的折扣价格折扣价格是通过用标签价减去实际的折扣而计算的
清单 二元合成
package syssetlfp;
import monsfunctorBinaryFunction;
public class Subtract implements BinaryFunction
{
public Object evaluate(Object left Object right)
{
return new Double(((Double)left)doubleValue() ((Double)right)doubleValue());
}
}
package syssetlfp;
import monsfunctorBinaryFunction;
import monsfunctorUnaryFunction;
public class BinaryFunctionUnaryFunction implements UnaryFunction
{
private BinaryFunction function;
public BinaryFunctionUnaryFunction(BinaryFunction f)
{
function=f;
}
public Object evaluate(Object obj)
{
return functionevaluate(objobj);
}
}
package syssetlfp;
import monsfunctor*;
import posite*;
import monsfunctoradapter*;
import monsfunctorUnaryFunction;
import reConstant;
import paratorMin;
public class TestC
{
public static void main(String[] args)
{
double discountRate = ;
double taxRate=;
double maxDiscount = ;
SETLItem item = new SETLItem();
itemsetPrice();
UnaryFunction calcDiscount =
new RightBoundFunction(new Multiply() new Double(discountRate));
Constant cap = new Constant(new Double(maxDiscount));
BinaryFunction calcActualDiscount =
new UnaryCompositeBinaryFunction (new Min() calcDiscount cap);
BinaryFunctionUnaryFunction calcActualDiscountAsUnary =
new BinaryFunctionUnaryFunction(calcActualDiscount);
BinaryFunction calcDiscountedPrice =
new UnaryCompositeBinaryFunction (new Subtract() new Identity() calcActualDiscountAsUnary);
BinaryFunctionUnaryFunction calcDiscountedPriceAsUnary =
new BinaryFunctionUnaryFunction(calcDiscountedPrice);
UnaryFunction calcTax =
new RightBoundFunction(new Multiply() new Double(+taxRate));
CompositeUnaryFunction calcNetPrice =
new CompositeUnaryFunction(calcTax calcDiscountedPriceAsUnary);
Double netPrice = (Double)calcNetPriceevaluate(new Double(itemgetPrice()));
Systemoutprintln(The net price is: + netPrice);
}
}
通过首先观察所使用的 Apache Functor 库中的三个标准仿函数开始分析和理解这段代码
UnaryCompositeBinaryFunction 仿函数取一个二元函数和两个一元函数作为输入首先计算后两个函数它们的输出作为输入传递给二元函数在清单 中对二元合成使用这个仿函数两次
Constant 仿函数的计算总是返回一个常量值(即在其构造时输入的值)不管以后任何计算消息中传递给它的参数是什么值在清单 中变量 cap 的类型为 Constant 并总是返回最大折扣数量
Identity 仿函数只是返回作为 evaluate 消息的输入参数传递给它的这个对象作为输出清单 显示 Identity 仿函数的一个实例该仿函数是在创建 calcDiscountedPrice 时作为一个一元仿函数创建和传递的同时在清单 中 evaluate 消息包含标签价格作为其参数这样 Identity 仿函数就返回标签价格作为输出
第一个二元合成在用计算 calcDiscount (通过对标签价格直接应用折扣率)和 cap 的 UnaryCompositeBinaryFunction 设置变量 calcActualDiscount 时是可见的这两个一元仿函数计算的输出传递给称为 Min 的内置二元仿函数它比较这两者并返回其中最小的值
这个例子显示了定制类 BinaryFunctionUnaryFunction 这个类适配一个二元仿函数使它像一元仿函数的接口就是说当这个类接收一个带有一个参数的 evaluate 消息时它在内部发送 (向其封装的二元函数)一个 evaluate 消息它的两个参数是作为输入接收的同一个对象因为 calcActualDiscount 是二元函数所以通过类型为 BinaryFunctionUnaryFunction 的 calcActualDiscountAsUnary 实例将它包装到一个一元仿函数接口中很快就可以看到包装 calcActualDiscount 为一元仿函数的理由
当用 UnaryCompositeBinaryFunction 设置变量 calcDiscountedPrice 时发生第二个二元合成 UnaryCompositeBinaryFunction 向新的 Identity 实例和 calcActualDiscountAsUnary 对象发送 evaluation 消息这两个消息的输入参数都是标签价格
这两个计算(它们分别得出标签价格和实际的折扣值)的输出传递给名为 Subtract 的定制二元仿函数当向后一个对象发送 evaluate 消息时它立即计算并返回两个参数之间的差距(这是商品的折扣价)这个二元仿函数也用定制的 BinaryFunctionUnaryFunction 包装为一个名为 calcDiscountedPriceAsUnary 的一元仿函数对象
与前面的情况一样代码通过两个 calcTax 一元仿函数(也在清单 中遇到)和 calcDiscountedPriceAsUnary (在前面一段中描述)创建 CompositeUnaryFunction 而以一个一元合成完成这样得到的 calcNetPrice 变为接收一个 evaluate 消息和一个参数(所讨论商品的标签价格)而在内部首先用这个参数计算 calcDiscountedPriceAsUnary 仿函数然后用前一个计算的输出作为参数计算 calcTax 仿函数
使用闭包实现业务规则
Apache Library 提供了各种不同的内置一元和二元仿函数它使得将业务逻辑编写为可以传递并且可以用不同的参数在不同的位置执行的对象变得非常容易在后面几节中我将使用一个简单的例子展示对一个类似问题的函数编程方式
假定一个特定的商品是否可以有折扣取决于该商品的类别和定价具体说只有 Category A 中定价高于 美元和 Category B 中定价高于 美元的商品才有资格打折清单 中的代码显示了一个名为 isEligibleForDiscount 的业务规则对象 ( UnaryPredicate )如果用一个 item 对象作为参数发送 evaluate 消息将返回一个表明是否可以对它打折的 Boolean
清单 一个函数业务规则对象
package syssetlfp;
import monsfunctorBinaryPredicate;
import monsfunctorUnaryPredicate;
public class BinaryPredicateUnaryPredicate implements UnaryPredicate
{
private BinaryPredicate bp;
public BinaryPredicateUnaryPredicate(BinaryPredicate prd)
{
bp=prd;
}
public boolean test(Object obj)
{
return bptest(objobj);
}
}
package syssetlfp;
import monsfunctor*;
import posite*;
import monsfunctoradapter*;
import monsfunctorUnaryFunction;
import reConstant;
import reIsEqual;
import paratorIsGreaterThanOrEqual;
import paratorMin;
import reIdentity;
public class TestD
{
public static void main(String[] args)
{
SETLItem item = new SETLItem();
itemsetPrice();
itemsetCategory(A);
SETLItem item = new SETLItem();
itemsetPrice();
itemsetCategory(A);
SETLItem item = new SETLItem();
itemsetPrice();
itemsetCategory(B);
UnaryFunction getItemCat =
new UnaryFunction()
{
public Object evaluate (Object obj)
{
return ((SETLItem)obj)getCategory();
}
};
UnaryFunction getItemPrice =
new UnaryFunction()
{
public Object evaluate(Object obj)
{
return new Double(((SETLItem)obj)getPrice());
}
};
Constant catA = new Constant(A);
Constant catB = new Constant(B);
Constant usd = new Constant(new Double());
Constant usd = new Constant(new Double());
BinaryPredicateUnaryPredicate belongsToCatA = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsEqual() getItemCat catA));
BinaryPredicateUnaryPredicate belongsToCatB = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsEqual() getItemCat catB));
BinaryPredicateUnaryPredicate moreThanUSD = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual() getItemPrice usd));
BinaryPredicateUnaryPredicate moreThanUSD = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual() getItemPrice usd));
UnaryOr isEligibleForDiscount = new UnaryOr(new UnaryAnd(belongsToCatA moreThanUSD)
new UnaryAnd(belongsToCatB moreThanUSD));
if (isEligibleForDiscounttest(item))
Systemoutprintln(Item # is eligible for discount!);
else
Systemoutprintln(Item # is not eligible for discount!);
if (isEligibleForDiscounttest(item))
Systemoutprintln(Item # is eligible for discount!);
else
Systemoutprintln(Item # is not eligible for discount!);
if (isEligibleForDiscounttest(item))
Systemoutprintln(Item # is eligible for discount!);
else
Systemoutprintln(Item # is not eligible for discount!);
}
}
使用 ComparableComparator
清单 中可能注意到的第一件事是我利用了名为 isEqual (用于检查所说商品的类别是否等于 A或者B ) 和 isGreaterThanOrEqual (用于检查所述商品的定价是否大于或者等于指定值对于 Category A 商品是 对于 Category B 商品是 ) 的内置二元谓词仿函数
您可能还记得在 清单 中原来必须传递 PriceComparator 对象(封装了比较逻辑)以使用 isGreaterThanOrEqual 仿函数进行价格比较不过在清单 中不显式传递这个 Comparator 对象如何做到不需要它? 技巧就是在没有指定该对象时 isGreaterThanOrEqual 仿函数(对这一点甚至是 IsEqual 仿函数)使用默认的 ComparableComparator 这个默认的 Comparator 假定两个要比较的对象实现了 javalangComparable 接口并对第一个参数(在将它类型转换为 Comparable 后)只是调用 compareTo 方法传递第二个参数作为这个方法的参数
通过将比较工作委派给这个对象本身对于 String 比较(像对 item 目录所做的)和 Double 比较(像对 item 价格所做的)可以原样使用默认的 Comparator String 和 Double 都是实现了 Comparable 接口的默认 Java 类型
将二元谓词适配为一元
可能注意到的第二件事是我引入了一个名为 BinaryPredicateUnaryPredicate 的新仿函数这个仿函数(类似于在 清单 中第一次遇到的 BinaryFunctionUnaryFunction 仿函数)将一个二元谓词接口适配为一元接口 BinaryPredicateUnaryPredicate 仿函数可以认为是一个带有一个参数的一元谓词它在内部用同一个参数的两个副本计算包装的二元谓词
isEligibleForDiscount 对象封装了一个完整的业务规则如您所见它的构造方式 ── 即通过将构造块从下到上放到一起以构成更复杂的块再将它们放到一起以构成更复杂的块等等 ── 使它本身天然地成为某种可视化的规则构造器最后的规则对象可以是任意复杂的表达式它可以动态地构造然后传递以计算底层业务规则
对集合操作
GoF Iterator 模式(请参阅 参考资料) 提供了不公开其底层表示而访问集合对象的元素的方法这种方法背后的思路是迭代与数据结构不再相关联 (即它不是集合的一部分)这种方式本身要使用一个表示集合中特定位置的对象并用一个循环条件 (在集合中增加其逻辑位置)以遍历集合中所有元素循环体中的其他指令可以检查和/或操作集合中当前 Iterator 对象位置上的元素在本例中我们对迭代没有什么控制(例如必须调用多少次 next 每次试图访问 next 元素时必须首先检查超出范围错误) 此外迭代器必须使用与别人一样的公共接口访问底层数据结构的成员这使得访问效率不高这种迭代器常被称为 外部迭代器(External Iterator)
FP 对这个问题采取了一种非常不同的方式集合类有一个高阶函数后者以一个仿函数作为参数并在内部对集合的每一个成员应用它在本例中因为迭代器共享了数据结构的实现所以您可以完成控制迭代此外迭代很快因为它可以直接访问数据结构成员这种迭代器常被称为 内部迭代器(internal Iterator)
Apache Functor 库提供了各种非严格地基于 C++ 标准模板库实现的内部 Iterator 它提供了一个名为 Algorithms 的 实用工具类这个类有一个名为 foreach 的方法 foreach 方法以一个 Iterator 对象和一个一元 Procedure 作为输入并对遍历 Iterator 时遇到的每一个元素(元素本身是作为过程的一个参数传递的)运行一次
使用内部迭代器
一个简单的例子将可以说明外部和内部 Iterator 的不同假定提供了一组 SETLItem 对象并要求累积列表中成本高于 美元的那些商品的定价清单 展现了完成这一工作的代码
清单 使用外部和内部迭代器
package syssetlfp;
import javautil*;
import monsfunctorAlgorithms;
import monsfunctorUnaryFunction;
import monsfunctorUnaryProcedure;
import reConstant;
import llectionFilteredIterator;
import paratorIsGreaterThanOrEqual;
import positeUnaryCompositeBinaryPredicate;
public class TestE
{
public static void main(String[] args)
{
Vector items = new Vector();
for (int i=; i<; i++)
{
SETLItem item = new SETLItem();
if (i%==)
itemsetPrice();
else
itemsetPrice(i);
itemsadd(item);
}
TestE t = new TestE();
Systemoutprintln(The sum calculated using External Iterator is: +
tcalcPriceExternalIterator(items));
Systemoutprintln(The sum calculated using Internal Iterator is: +
tcalcPriceInternalIterator(items));
}
public int calcPriceExternalIterator(List items)
{
int runningSum = ;
Iterator i = erator();
while (ihasNext())
{
int itemPrice = ((SETLItem)inext())getPrice();
if (itemPrice >= )
runningSum += itemPrice;
}
return runningSum;
}
public int calcPriceInternalIterator(List items)
{
Iterator i = erator();
UnaryFunction getItemPrice =
new UnaryFunction()
{
public Object evaluate (Object obj)
{
return new Double(((SETLItem)obj)getPrice());
}
};
Constant usd = new Constant(new Double());
BinaryPredicateUnaryPredicate moreThanUSD = new BinaryPredicateUnaryPredicate
(new UnaryCompositeBinaryPredicate(new IsGreaterThanOrEqual() getItemPrice usd));
FilteredIterator fi = new FilteredIterator(i moreThanUSD);
Summer addPrice = new Summer();
Algorithmsforeach(fi addPrice);
return addPricegetSum();
}
static class Summer implements UnaryProcedure
{
private int sum=;
public void run(Object obj)
{
sum += ((SETLItem)obj)getPrice();
}
public int getSum()
{
return sum;
}
}
}
在 main() 方法中设置一个有 种商品的列表其中奇数元素的价格为 美元(在真实应用程序中将使用调用 JDBC 获得的 ResultSet 而不是本例中使用的 Vector )
然后用两种不同的方法对列表执行所需要的操作 calcPriceExternalIterator 和 calcPriceInternalIterator 正如其名字所表明的前者基于 ExternalIterator 而后者基于 InternalIterator 您将关心后一种方法因为所有 Java 开发人员都应该熟悉前者注意 InternalIterator 方法使用由 Apache Functor 库提供的两个结构
第一个结构称为 FilteredIterator 它取一个迭代器加上一个一元 谓词 作为输入并返回一个带有所感兴趣的属性的 Iterator 这个属性给出了在遍历 Iterator 时遇到的每一个满足在 谓词 中规定的条件的元素(因此由 FilteredIterator 的一个实例返回的 Iterator 可以作为 FilteredIterator 的第二个实例的输入传递以此类推以设置过滤器链用于根据多种标准分步挑出元素)在本例中只对满足 一元谓词 大于或等于 美元规则的商品感兴趣这种规则是在名为 moreThanUSD 的 BinaryPredicateUnaryPredicate 中规定的我们在 清单 中第一次遇到了它
Apache Functor 库提供的第二个结构是名为 Algorithms 的实用程序类在 前面描述 过这个类在这个例子中名为 Summer 的一元过程只是包含传递给它的 SETLItem 实例的定价并将它添加到(本地)运行的 total 变量上这是一个实现了前面讨论的内部迭代器概念的类
使用仿函数进行集合操纵
我讨论了用仿函数和高阶函数编写模块的大量基础知识我将用最后一个展示如何用仿函数实现集合操纵操作的例子作为结束
通常有两种描述集合成员关系的方式第一种是完全列出集合中的所有元素这是 Java 编程人员传统上使用的机制 ── javautilSet 接口提供了一个名为 add(Object) 的方法如果作为参数传递到底层集合中的对象还未存在的话该方法就添加它
不过当集合中的元素共享某些公共属性时通过声明惟一地标识了集合中元素的属性可以更高效地描述集合的成员关系例如后一种解决方案适合于集合成员的数量很大以致不能在内存中显式地维护一个集合实现(像前一种方式那样)的情况
在这种情况下可以用一个一元 谓词 表示这个集合显然一个一元 谓词 隐式定义了一组可以导致谓词计算为 true的所有值(对象)事实上所有集合操作都可以用不同类型的谓词组合来定义清单 中展示了这一点
清单 使用仿函数的集合操作
package syssetlfp;
import monsfunctorUnaryPredicate;
import positeUnaryAnd;
import positeUnaryNot;
import positeUnaryOr;
public class SetOps
{
public static UnaryPredicate union(UnaryPredicate up UnaryPredicate up)
{
return new UnaryOr(up up);
}
public static UnaryPredicate intersection(UnaryPredicate up UnaryPredicate up)
{
return new UnaryAnd(up up);
}
public static UnaryPredicate difference(UnaryPredicate up UnaryPredicate up)
{
return new UnaryAnd(up new UnaryNot(up));
}
public static UnaryPredicate symmetricDifference(UnaryPredicate up
UnaryPredicate up)
{
return difference(union(up up) intersection(up up));
}
}
用一元 谓词 来描述集合 并集(union) 和 交集(intersection) 操作的定义应当是明确的如果一个对象至少使指示两个集合的两个一元 谓词 中的一个计算为 true那么这个对象就属于两个集合的并集(逻辑 Or )如果它使两个一元 谓词 都计算为 true那么它就属于两个集合的交集(逻辑 And )
两个集合的差在数学上定义为属于第一个集合但是不属于第二个集合一组元素根据这个定义静态方法 difference 也容易理解
最后两个集合的对称差(symmetric difference)定义为只属于两个集合中的一个(或者两个都不属于)的所有元素这可以取两个集合的并集然后从中删除属于两个集合的交集的元素得到就是说它是对原来的集合(在这里是一元 谓词 )使用 union 和 intersection 操作分别得到的两个集合的 difference 操作后一个定义解释了为什么用前三种方法作为第四个方法中的构建块
结束语
模块化是任何平台上高生产率和成功的编程的关键这一点早已被认识到了Java 开发人员的问题是模块化编程不仅是将问题分解它还要求能将小的解决方案粘接到一起成为一个有效的整体由于这种类型的开发继承了函数编程范型在 Java 平台上开发模块化代码时使用函数编程技术应是很自然的事
在本文中我介绍了两种函数编程技术它们可以容易地结合到 Java 开发实践中正如从这里看到的闭包和高阶函数对于 Java 开发人员来说并不是完全陌生的它们可以有效地结合以创建一些非常有用的模块化解决方案