Java 8 的函数式接口

lambda 被用于接收特定接口的地方, Java 正是以此来决定调用哪个方法的。

Java 并不对命名加以限制,有些语言则不然。

唯一的限制是所用的接口必须要明确,这通常意味着它应该有且仅有一个抽象方法。(实际上会更复杂一些,因为有些方法不算在内。)

这样的接口就是SAM (single abstract method, 单一抽象方法)类型,被称为函数式接口 (functional interface)。

请注意 lambda 井不仅仅用于函数,在标准Java 8 中可以使用许多函数式接口,虽然它们并不都与函数相关。

核心接口

以下是一些比较重要的接口。

  • java.util.function.Function

与本章开发的 Function 很相似。为了让方法更有用,方法的参数中加了一个通配符。

  • java.util.function.Suppluer 等价于无参函数。

在函数式编程里,它就是一个常量,所以一开始你可能会觉得它没那么有用,但是它有两个特定的用途:

首先,如果它不是引用透明的(不是一个纯函数),便可以用于提供可变数据,例如时间或者随机数。( 我们才不用这么不函数式的东西!)

第二个用途更有意思,它允许惰性求值。我们会在后续的章节中频繁探讨这个主题。

  • java.util.function.Consumer 并不是函数,而是作用。

( 这里并不是指副作用。使用Consumer 的唯一结果就是作用,因为它什么东西也不返回。)

  • java.lang.Runnable

也可以用于不接收任何参数的作用。一般最好为其创建一个指定的接口,因为 Runnable 应该用于线程,并且大多数语法检测工具都会在它被挪作他用时向你投诉。

Java 定义了许多其他的函数式接口(在 java.util.function 包里有43 个),对于函数式编程而言大都没有什么用。

其中有许多处理原始类型和其他的双参函数,还有用于操作(接收两个类型相同参数的函数〉的特定版本。

我并不会在本书中讲解太多标准的Java 8 函数,我是有意而为之的。本书并不是一本关于Java 8 的书,而是一本关于函数式编程,正好以Java 为例的书。

你要学的是如何构造东西而非使用己经提供的组件。

一旦掌握了这些概念,你便可以自行决定是使用自己的函数还是使用标准的Java 8 函数。我们的Function 与Java 8 中的Function 相似。

为了简化本书所示的代码,它并没有为参数使用通配符。

另一方面, Java 8 的 Function 也没有定义 compose 和 andThen 这样的高阶函数, 它只有方法。除了这些不同以外,这些 Function 实现都是可互换的。

调试 lambda

lambda 的使用推动了一种写代码的新风格。

曾经写成许多小短行的代码现在经常被替换为如下的一行代码:

public <T> T ifElse(List<Boolean> conditions , List<T> ifTrue , T ifFalse ) {
    return conditions.zip(ifTrue).flatMap(x -> x.first(y -> y._1))
    .map(x-> x._2).getOrElse(ifFalse);
}

(ifElse 方法的实现在此由于页宽的限制而被拆成两行,但是在代码编辑器中它其实在同一行中。〉

在Java 5 到7 中,这段代码只能用非lambda 的方式编写,如清单2.3 所示。

此处省略,反正是一堆代码。

如何便于调试

显然, lambda 版本更加易读也容易修改。一般都认为Java 8 之前的版本复杂得无法令人接受。

但是当调试的时候, lambda 的版本问题更多。

如果一行相当于原本的20 行代码,那么如何才能有效地设置断点以找到潜在的错误呢?

问题在于井非所有的调试器都强大到可以轻松应付 lambda 虽然问题总会随时间慢慢被解决,但同时你也需要找出其他的方案。

一个简单的方案就是把单行的版本拆分为多行,如下所示:

public <T> T ifElse(List<Boolean> conditions , List<T> ifTrue , T ifFalse ) {
    return conditions.zip(ifTrue)
    .flatMap(x -> x.first(y -> y._1))
    .map(x-> x._2) 
    .getOrElse(ifFalse);
}

这样你就可以为每一行设置断点。不仅非常实用,而且让代码更易读(还让书的排版更加容易) 。

但是它并不能解决我们的问题,因为每一行仍然有许多传统调试器不能完全理解的内容。

为了降低这个问题的严重程度,对每个组件进行全面的单元测试非常重要,这里的组件指的是每个方法,以及作为参数传递给方法的每个函数。

这正是函数式编程的亮点: 如果组件都不会出错,那么整个程序也不会。

在命令式编程中,组件可能在测试中运行良好,但在生产环境中由于一些不确定的因素而出错误。如果组件的行为依赖于外部条件,那么你就无法完整地测试它。 即使每个组件都没有问题,多个组件的组合仍然可能会导致程序表现欠佳。如果组件的行为确定,那么整个组合也将确定。

许多地方仍然可能出现错误。程序可能会由于组件复合错误而不按预期工作。不过实现错误并不会导致意外崩溃。如果程序崩溃了,那它永远都将如此,例如传递一个null 给Tuple 的构造函数。捕捉这种类型的错误无须使用调试器。

个人收获

函数式编程是一种思维的转变,可以让我们的编程变得更加优雅。

当然编辑器面对这种新生的方式,有时候表现不是很良好,所以需要完整的测试,从而保证代码的正确性。

参考资料

《java 函数式编程》