此篇文章主要基于对java.util.function
包的javadoc翻译,谈谈Java对函数式编程的支持,亦有助于学习Stram API。
1 什么是函数式编程?
在计算机科学中,函数式编程(Functional programing)是一种将计算(Computation)视为数学函数的评估,避免改变状态(changing-state)和可变数据(mutable data)的编程范式(一种构建计算机程序结构和元素的方式)。
In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. 摘自维基百科
2 java.util.function包
Java自1.8添加java.util.function包,以支持函数式编程;其将计算抽象为函数式接口(Functional interface),为lambda表达式(lambda expressions)和方法引用(method references)提供了目标类型。每一个函数式接口含有单个抽象方法,称为该函数式接口的函数式方法(functional method),lambda表达式的参数和返回类型与之匹配或适应。在多种上下文中,函数式接口可提供一个目标类型,如赋值、方法调用和转型上下文:
// 赋值上下文
Predicate<String> p = String::isEmpty;
// 方法调用上下文
stream.filter(e -> e.getSize() > 10)...
// 转型上下文
stream.map((ToIntFunction) e -> e.getSize())
此包中的接口是JDK使用的通用函数式接口,也可供用户代码使用。虽然它们没有确定可以适应lambda表达式的一整套函数形状,但它们足以满足常见要求。为特定目的提供的其他功能接口(例如FileFilter)在使用它们的包中定义。
此包中的接口都被注解为FunctionalInterface注解。此注释不是编译器将接口识别为功能接口的必要条件,而仅是帮助捕获设计意图(design intent)并获得编译器帮助识别意外违反设计意图的帮助。
函数式接口通常表示类似函数(functions)、操作(actions)或谓词(predicates)的抽象概念。在记录函数式接口或引用类型为函数式接口的变量时,通常直接引用那些抽象概念,例如用“这个函数”而不是“这个对象表示的函数”。当一个API方法以例如“将提供的函数应用于…”这种方式接受或返回函数式接口时,这被理解一个对实现适当函数式接口的对象的非null引用,除非明确制定了潜在的无效性。
此包中的函数式接口遵循可拓展的命名约定,如下所示:
基本函数形式(function shapes),包括Function(从T到R的一元函数),Consumer(从T到void的一元函数),Predicate(从T到boolean的一元函数)和Supplier(到R的0元函数)。
含有基于最常用的方式的自然元数的函数形式。可通过元数(Arity)前缀修改基本形式以表示不同的元数,如BiFunction(从T和U到R的二元函数)中所示。
派生函数形式,它们拓展了基本函数形式,包括UnaryOperator(拓展了Function)和BinaryOperator(拓展了BiFunction)。
函数式接口的类型参数可以专用与具有类型前缀的基本类型(primitives)。为具有泛型返回类型和泛型参数的类型专门化(specialize)返回类型,我们加上前缀ToXxx,如ToIntFunction中所示。另外,类型参数从左到右是专门化的,例如DoubleConsumer和ObjIntConsumer(类型前缀
Obj
被用于表示我们不想专门化此参数,但希望专门化下一个参数)。可以结合这些模式,如IntToDoubleFunction中所示。若所有参数都有专门化前缀,则可以省略元数前缀,如ObjIntConsumer中所示。
3 Lambda表达式
Java 8 引入的功能中,最让人期盼的是lambda表达式。Lambda表达式以字面量的形式把少量代码直接写在程序中,而且让Java编程更符合函数式风格。其实,lambda表达式的很多功能都能使用回调和处理程序等模式来实现,但使用的句法总是非常冗长,尤其是,就算只需要在回调中编写一行代码,也要完整定义一个新类型。Lambda表达式的句法是一个参数列表和方法主体,如下所示:
(p, q) -> { /* 方法主体 */}
这种句法能通过一种十分紧凑的方式表示简单的方法,而且能很大程度上避免使用匿名类。组成方法的各个部分,lambda表达式几乎都有,不过显然,lambda表达式没有名称,有些发开着喜欢吧lambda表达式当成“匿名方法”。
上部分我们知道函数式接口只包含一个抽象方法,有些开发者喜欢使用“单一抽象方法”(Single Abstract Method, SAM)类型这个属于表示lambda表达式转换得到的接口类型。我们可以使用匿名类或具名类来实现它,但Java从1.8开始支持lambda表示式,使您可以更紧凑地表达单方法类的实例。例如,我们基于java.util.function.BiFunction
实现一个整数加法函数,若使用具名类,您可能会这样写:
import java.util.function.BiFunction;
public final class AddFunction implements BiFunction<Integer, Integer, Integer> {
@Override
public Integer apply(Integer first, Integer second) {
return first + second;
}
}
是不是代码量较大?使用lambda,我们可以以匿名类的方式实现AddFunction
同样的函数化实例,如下:
BiFunction<Integer, Integer, Integer> addFunction
= (first, second) -> (first + second);
是不是清爽🍃了很多?
javac
遇到lambda 表达式时会把它解释为一个方法的主体,这个方法具有特定的签名。不过,是那个方法呢?
为了解决这个问题,javac会查看周围的代码,lambda 表达式必须满足以下条件才算是合法的Java代码:
- lambda 表达式必须出现在期望使用接口类型实例的地方;
- 期望使用的接口类型必须只有一个强制方法;
- 这个强制方法的签名要完全匹配lambda表达式。
如果满足上述条件,编译器会创建一个类型,实现期望使用的接口,然后把lambda表达式的主体当作强制方法的实现。说的稍微复杂点,这个作是为了保持Java类型系统的名义(基于名称)纯粹性。也就是说,lambda表达式会被转换成正确接口类型的实例。