Java FP: Java中函数式编程的谓词函数(Predicates)第一部分

你一直在听说函数式编程将称霸整个编程届,而自己仍然沉浸在普通的Java里?请不要担心,因为你已经在日常Java代码中加入了函数式编程的特性。此外,函数式编程很有趣,能够帮你节省多行代码并且降低错误率。

什么是谓词函数?

许久之前,那时我还在用Java 1.4进行编码,当我第一次发现Apache Commons Collections,便爱上了谓词函数。Apache Commons Collections里的谓词函数仅仅只是一个只有一个方法的接口:

evaluate(Object object): boolean

这就是谓词函数,输入一个对象,返回true或者false。最近诞生了类似Apache Commons Collections的持有Apache 2.0许可的Google Guava。在Google Guava中,定义了Predicate接口,该接口包含一个带有泛型参数的方法:

apply(T input): boolean

如果想在程序中使用谓词函数,只需要利用自己的逻辑实现该接口即可。

一个简单的例子

先举一个例子,假设你有一个订单列表,每个订单用PurchaseOrder表示,PurchaseOrder中包含日期,顾客和状态。不同的用例会要求你有不同的输出,比如获取某个顾客所有、等待发货、已发货、已交付或者过去一个小时内完成的订单。当然你可以在循环中使用if判断实现这些功能:

//List<PurchaseOrder> orders...
public List<PurchaseOrder> listOrdersByCustomer(Customer customer) {
    final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>();
    for (PurchaseOrder order : orders) {
        if (order.getCustomer().equals(customer)) {
            selection.add(order);
        }
    }
    return selection;
}

以上是获取某个顾客所有订单的代码。不同的功能需要编写多个类似的循环:

public List<PurchaseOrder> listRecentOrders(Date fromDate) {
        final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>();
        for (PurchaseOrder order : orders) {
            if (order.getDate().after(fromDate)) {
                selection.add(order);
            }
        }
        return selection;
}

这些重复代码非常明显:除了if的判断条件之外没有任何差异(译者注:方法参数可归为判断条件)。采用谓词函数的思想在于,利用传入到函数内的谓词的调用替代if语句块里的硬编码的判断条件。这意味着,你只需编写一遍带有谓词函数作为参数的方法,就可以覆盖所有的甚至你还不知道的测试用例:

public List<PurchaseOrder> listOrders(Predicate<PurchaseOrder> condition) {
    final List<PurchaseOrder> selection = new ArrayList<PurchaseOrder>();
    for (PurchaseOrder order : orders) {
        if (condition.apply(order)) {
            selection.add(order);
        }
    }
    return selection;
}

如果需要考虑到复用,可以把谓词函数声明成一个单独的类,否则可以把谓词声明成匿名类:

final Customer customer = new Customer("BruceWaineCorp");
final Predicate<PurchaseOrder> condition = new Predicate<PurchaseOrder>() {
    public boolean apply(PurchaseOrder order) {
        return order.getCustomer().equals(customer);
    }
};

如果你的使用过真正意义上的函数式语言(Scala, Clojure, Haskell等)的朋友看到这些代码,可能会觉得在处理通用功能时代码显得非常冗余。然而我们已经习惯于Java冗长的语法,并且我们有强大的工具(自动补齐、重构)帮助我们适应它,这使得我们的Java项目无法一夜之间转变成其他语言的项目。

谓词函数是集合类的好朋友

回到之前的例子,我们写了一个覆盖了所有用例的循环,我们为共性的抽离感到开心,但是你的朋友依然会嘲笑你。幸运的是,Apache或者Google的API都提供了你想要的东西,还特别提供了一个类似java.util.Collections的命名为Collections2的类(名字不是很新颖)。

这个类提供给了与我们先前编写的代码功能类似的filter()函数,所以我们可以把方法重构成无循环的版本:

public Collection<PurchaseOrder> selectOrders(Predicate<PurchaseOrder> condition) {
    return Collections2.filter(orders, condition);
}

实际上,这个方法返回了一个过滤后的视图:

返回的集合是未经过滤的集合(输入的集合)的真实缩影(译者注:先前版本的函数返回的集合是输入集合的一个子集的拷贝),更改其中一个集合会影响另一个集合。

这意味着这种方式将使用更少的内存,因为不会把原始集合的内容拷贝到返回的集合中。

在一个类似的场景中,我们可以要求返回在给定的迭代器之上过滤好的只符合谓词函数的元素的迭代器(装饰模式)。

Iterator filteredIterator = Iterators.filter(unfilteredIterator, condition);

从Java 5开始,Iterable接口和循环使用起来非常方便,所以我们更倾向于使用以下写法:

public Iterable<PurchaseOrder> selectOrders(Predicate<PurchaseOrder> condition) {
    return Iterables.filter(orders, condition);
}
// you can directly use it in a foreach loop, and it reads well:
for (PurchaseOrder order : orders.selectOrders(condition)) {
    //...
}

现成的谓词函数

为了使用谓词,我们必须声明自己的谓词接口,或者为应用程序中使用到的谓词参数都声明一个类。这是可行的,然而从类似Guava以及Commons的API中使用标准谓词接口的好处是:你可以结合这类API提供的大量优秀组件实现你自己的谓词函数。

如果你需要的是判断一个对象是否为空或者不为空的条件,你不需要自己实现一个谓词函数,只需要使用现成的谓词就可以了:

// gives you a predicate that checks if an integer is zeroPredicate
<Integer> isZero = Predicates.equalTo(0);
// gives a predicate that checks for non null objects
Predicate<String> isNotNull = Predicates.notNull();
// gives a predicate that checks for objects that are instanceof the given Class
Predicate<Object> isString = Predicates.instanceOf(String.class);

对于给定的谓词,你可以反转它(返回相反的返回值,比如true变成false):

Predicates.not(predicate);

利用AND,OR操作结合多个谓词:

Predicates.and(predicate1, predicate2);
Predicates.or(predicate1, predicate2);
// gives you a predicate that checks for either zero or null
Predicate<Integer> isNullOrZero = Predicates.or(isZero, Predicates.isNull());

当然你也可以拥有返回固定值(true或者false)的特殊谓词(译者注:只返回true即为逻辑学中的永真式,反之为永假式)。这些谓词非常有用,我们可以在之后的例子中证明:

Predicates.alwaysTrue();
Predicates.alwaysFalse();

如何定位谓词

起先,我经常编写匿名的谓词类,后来这些谓词总是频繁使用,所以我会将匿名的谓词升级成实体类、内部类等。

顺便提一下,如何定位谓词呢?请参考Robert C. Martin的文章 Common Closure Principle (CCP)中提到的一段话 :

一起变化的类,属于一个整体。

因为谓词总是对一个特定类型的对象进行操作,我喜欢将谓词重新定位为谓词操作的参数的类型。比如,类CustomerOrderPredicate,PendingOrderPredicate 和RecentOrderPredicate 应该被防止在同一个包下或者子包下(如果你有很多包),而不是把代码写到这些谓词所操作的主体PurchaseOrder里。另一个选择是,将谓词声明成它们要操作的主体类型的内部类。显然,谓词与主体对象是非常耦合的。

资源

这里有本篇文章的例子源代码:cyriux_predicates_part1 (zip)

在下一小节,我们着重观察谓词函数如何简化测试、谓词如何与域驱动设计里的标准相联系,以及一些能让你高效利用谓词函数的额外知识。

参考文献

A touch of functional style in plain Java with predicates – Part 1 from our JCG partner Cyrille Martraire at the Cyrille Martraire’s blog

时间: 2024-11-03 21:38:19

Java FP: Java中函数式编程的谓词函数(Predicates)第一部分的相关文章

Java FP: Java中函数式编程的谓词函数(Predicates)第二部分

在上一篇文章中我们介绍了谓词函数.通过一个简单的只带一个返回值是true或者false的函数的接口,把函数式编程语言的优势带入到了类似Java的面向对象编程语言中.这一小节,我们将会介绍一些高级特性,方便你高效利用谓词函数. 测试 在测试代码中使用谓词的优势尤为明显.当你需要测试一个混合了数据结构与某些条件逻辑的方法时,通过使用谓词,你可以先单独测试数据结构,再测试条件逻辑. 第一步,先利用永真谓词或者永假谓词屏蔽用于判断的逻辑,将注意力集中在测试数据结构上: 1 // check with t

Java FP: Java中函数式编程的Map和Fold(Reduce)

原文链接 作者:  Cyrille Martraire  译者: 李璟(jlee381344197@gmail.com) 在函数式编程中,Map和Fold是两个非常有用的操作,它们存在于每一个函数式编程语言中.既然Map和Fold操作如此强大和重要,但是Java语言缺乏Map和Fold机制,那么该如何解释我们使用Java完成日常编码工作呢?实际上你已经在Java中利用手动编写循环的方式实现了Map和Fold操作(译者注:许多动态语言如python都提供了内置的实现). 免责声明:本篇文章仅仅只是

Java函数式编程(一):你好,Lambda表达式_java

第一章 你好,lambda表达式! 第一节 Java的编码风格正面临着翻天覆地的变化. 我们每天的工作将会变成更简单方便,更富表现力.Java这种新的编程方式早在数十年前就已经出现在别的编程语言里面了.这些新特性引入Java后,我们可以写出更简洁,优雅,表达性更强,错误更少的代码.我们可以用更少的代码来实现各种策略和设计模式. 在本书中我们将通过日常编程中的一些例子来探索函数式风格的编程.在使用这种全新的优雅的方式进行设计编码之前,我们先来看下它到底好在哪里. 改变了你的思考方式 命令式风格--

Python中的函数式编程

虽然人们总把Python当作过程化的,面向对象的语言,但是他实际上包含了函数化编程中,你需要的任何东西.这篇文章主要讨论函数化编程的一般概念,并说明用Python来函数化编程的技术. 我们最好从艰难的问题开始出发:"到底什么是函数化编程呢?"其中一个答案可能是这样的,函数化编程就是你在使用Lisp这样的语言时所做的(还有Scheme,Haskell,ML,OCAML,Mercury,Erlang和其他一些语言).这是一个保险的回答,但是它解释得并不清晰.不幸的是对于什么是函数化编程,很

JavaScript的函数式编程,你了解吗?

探索函数式编程,通过它让你的程序更具有可读性和易于调试 当 Brendan Eich 在 1995 年创造 JavaScript 时,他原本打算将 Scheme 移植到浏览器里 .Scheme 作为 Lisp 的方言,是一种函数式编程语言.而当 Eich 被告知新的语言应该是一种可以与 Java 相比的脚本语言后,他最终确立了一种拥有 C 风格语法的语言(也和 Java 一样),但将函数视作一等公民.而 Java 直到版本 8 才从技术上将函数视为一等公民,虽然你可以用匿名类来模拟它.这个特性允

函数式编程初探

诞生50多年之后,函数式编程(functional programming)开始获得越来越多的关注. 不仅最古老的函数式语言Lisp重获青春,而且新的函数式语言层出不穷,比如Erlang.clojure.Scala.F#等等.目前最当红的Python.Ruby.Javascript,对函数式编程的支持都很强,就连老牌的面向对象的Java.面向过程的PHP,都忙不迭地加入对匿名函数的支持.越来越多的迹象表明,函数式编程已经不再是学术界的最爱,开始大踏步地在业界投入实用. 也许继"面向对象编程&qu

函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式 - 响应式编程 [Android RxJava2](这到底是什么)第三部分

本文讲的是函数式接口.默认方法.纯函数.函数的副作用.高阶函数.可变的和不可变的.函数式编程和 Lambda 表达式 - 响应式编程 [Android RxJava2](这到底是什么)第三部分, 太棒了,我们又来到新的一天.这一次,我们要学一些新的东西让今天变得有意思起来. 大家好,希望你们都过得不错.这是我们的 RxJava2 Android 系列的第三篇文章. 第一部分 第二部分 在这篇文章中,我们将讨论函数式的接口,函数式编程,Lambda 表达式以及与 Java 8 的相关的其它内容.这

函数式编程

诞生50多年之后,函数式编程(functional programming)开始获得越来越多的关注. 不仅最古老的函数式语言Lisp重获青春,而且新的函数式语言层出不穷,比如Erlang.clojure.Scala.F#等等.目前最当红的Python.Ruby.Javascript,对函数式编程的支持都很强,就连老牌的面向对象的Java.面向过程的PHP,都忙不迭地加入对匿名函数的支持.越来越多的迹象表明,函数式编程已经不再是学术界的最爱,开始大踏步地在业界投入实用. 也许继"面向对象编程&qu

准备充分了嘛就想学函数式编程?(Part 6)

本文讲的是准备充分了嘛就想学函数式编程?(Part 6), 第一步,理解函数式编程概念是最重要的一步,同时也是最难的一步.如果你从正确的角度或方法来理解的话,它也未必会有那么难. 回顾之前的部分: Part 1, Part 2, Part 3, Part 4, Part 5 现在该做什么? 现在你已经学会了所有这些新东西了,你可能在想,"现在该干什么?我如何在日常编程中使用它?" 这得看情况.如果你会使用纯函数式语言(如 Elm 或 Haskell)编程,那么你可以尝试所有这些想法.这