20190825 On Java8 第十三章 函数式编程
第十三章 函数式编程
函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。
OO
(object oriented,面向对象)是抽象数据,FP
(functional programming,函数式编程)是抽象行为。
Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。这对 Java 来说是一个巨大的改进。因为这允许你编写更简洁明了,易于理解的代码。
没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。
Lambda表达式
Lambda 表达式是使用最小可能语法编写的函数定义:
- Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。
- Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。
任何 Lambda 表达式的基本语法是:
- 参数。
- 接着
->
,可视为“产出”。 ->
之后的内容都是方法体。
递归
递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。
方法引用
Runnable接口
Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run() 不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable。
class Go {
static void go() {
System.out.println("Go::go()");
}
}
public class RunnableMethodReference {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("Anonymous");
}
}).start();
new Thread(() -> System.out.println("lambda")).start();
new Thread(Go::go).start();
}
}
未绑定的方法引用
未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用之前,我们必须先提供对象。
使用未绑定的引用时,函数方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 理由是:你需要一个对象来调用方法。
class This {
void two(int i, double d) {
System.out.println("This two " + i + " , " + d);
}
void three(int i, double d, String s) {
System.out.println("This three " + i + " , " + d + " , " + s);
}
void four(int i, double d, String s, char c) {
System.out.println("This three " + i + " , " + d + " , " + s + " , " + c);
}
}
interface TwoArgs {
void call2(This athis, int i, double d);
}
interface ThreeArgs {
void call3(This athis, int i, double d, String s);
}
interface FourArgs {
void call4(This athis, int i, double d, String s, char c);
}
public class MultiUnbound {
public static void main(String[] args) {
TwoArgs twoargs = This::two;
ThreeArgs threeargs = This::three;
FourArgs fourargs = This::four;
This athis = new This();
twoargs.call2(athis, 11, 3.14);
threeargs.call3(athis, 11, 3.14, "Three");
fourargs.call4(athis, 11, 3.14, "Four", 'Z');
}
}
构造函数引用
class Dog {
String name;
int age = -1; // For "unknown"
Dog() {
name = "stray";
}
Dog(String nm) {
name = nm;
}
Dog(String nm, int yrs) {
name = nm;
age = yrs;
}
@Override
public String toString() {
return "Dog [name=" + name + ", age=" + age + "]";
}
}
interface MakeNoArgs {
Dog make();
}
interface Make1Arg {
Dog make(String nm);
}
interface Make2Args {
Dog make(String nm, int age);
}
public class CtorReference {
public static void main(String[] args) {
MakeNoArgs mna = Dog::new; // [1]
Make1Arg m1a = Dog::new; // [2]
Make2Args m2a = Dog::new; // [3]
Dog dn = mna.make();
System.out.println(dn);
Dog d1 = m1a.make("Comet");
System.out.println(d1);
Dog d2 = m2a.make("Ralph", 4);
System.out.println(d2);
}
}
函数式接口
方法引用和 Lambda 表达式必须被赋值,同时编译器需要识别类型信息以确保类型正确。
怎么知道传递给方法的参数的类型?
为了解决这个问题,Java 8 引入了 java.util.function
包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。
在编写接口时,可以使用 @FunctionalInterface
注解强制执行此“函数式方法”模式。
@FunctionalInterface
注解是可选的。接口中如果有多个方法则会产生编译时错误消息。
Java 8 允许我们以简便的语法为接口赋值函数。
java.util.function
包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。这主要是因为基本类型会产生一小部分接口。 如果你了解命名模式,顾名思义就能知道特定接口的作用。
以下是基本命名准则:
- 如果只处理对象而非基本类型,名称则为
Function
,Consumer
,Predicate
等。参数类型通过泛型添加。 - 如果接收的参数是基本类型,则由名称的第一部分表示,如
LongConsumer
,DoubleFunction
,IntPredicate
等,但基本Supplier
类型例外。 - 如果返回值为基本类型,则用
To
表示,如ToLongFunction <T>
和IntToLongFunction
。 - 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用
UnaryOperator
,两个参数使用BinaryOperator
。 - 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。
- 如果接收的两个参数类型不同,则名称中有一个
Bi
。
下表描述了 java.util.function
中的目标类型(包括例外情况):
特征 | 函数式方法名 | 示例 |
---|---|---|
无参数; 无返回值 |
Runnable(java.lang) run() |
Runnable |
无参数; 返回类型任意 |
Supplier get() getAs类型() |
Supplier BooleanSupplier IntSupplier LongSupplier DoubleSupplier |
无参数; 返回类型任意 |
Callable(java.util.concurrent) call() |
Callable |
1 参数; 无返回值 |
Consumer accept() |
Consumer IntConsumer LongConsumer DoubleConsumer |
2 参数Consumer | BiConsumer accept() |
BiConsumer<T,U> |
2 参数Consumer; 1 引用; 1 基本类型 |
Obj类型Consumer accept() |
ObjIntConsumer ObjLongConsumer ObjDoubleConsumer |
1 参数; 返回类型不同 |
Function apply() To类型和类型To类型 applyAs类型() |
Function<T,R> IntFunction LongFunction DoubleFunction ToIntFunction ToLongFunction ToDoubleFunction IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
1 参数; 返回类型相同 |
UnaryOperator apply() |
UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
2 参数类型相同; 返回类型相同 |
BinaryOperator apply() |
BinaryOperator IntBinaryOperator LongBinaryOperator DoubleBinaryOperator |
2 参数类型相同; 返回整型 |
Comparator(java.util) compare() |
Comparator |
2 参数; 返回布尔型 |
Predicate test() |
Predicate BiPredicate<T,U> IntPredicate LongPredicate DoublePredicate |
参数基本类型; 返回基本类型 |
类型To类型Function applyAs类型() |
IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
2 参数类型不同 | Bi操作 (不同方法名) |
BiFunction<T,U,R> BiConsumer<T,U> BiPredicate<T,U> ToIntBiFunction<T,U> ToLongBiFunction<T,U> ToDoubleBiFunction |
在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名,而不是你的方法名。
高阶函数
高阶函数(Higher-order Function)只是一个消费或产生函数的函数。
闭包
Java 8 提供了有限但合理的闭包支持。
从 Lambda 表达式引用的局部变量必须是 final 或者是等同 final 效果的。
等同 final 效果(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是 final 的,但是因变量值没被改变过而实际有了 final 同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是 final 的。
应用于对象引用的 final 关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。
函数组合
一些 java.util.function
接口中包含支持函数组合的方法。
组合方法 | 支持接口 |
---|---|
andThen(argument) 根据参数执行原始操作 |
Function BiFunction Consumer BiConsumer IntConsumer LongConsumer DoubleConsumer UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator BinaryOperator |
compose(argument) 根据参数执行原始操作 |
Function UnaryOperator IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
and(argument) 短路逻辑与原始断言和参数断言 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
or(argument) 短路逻辑或原始断言和参数断言 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
negate() 该断言的逻辑否断言 |
Predicate BiPredicate IntPredicate LongPredicate DoublePredicate |
public class FunctionComposition {
static Function<String, String> f1 = s -> {
System.out.println(s);
return s.replace('A', '_');
}, f2 = s -> s.substring(3), f3 = s -> s.toLowerCase(), f4 = f1.compose(f2).andThen(f3);
public static void main(String[] args) {
System.out.println(f4.apply("GO AFTER ALL AMBULANCES"));
}
}
public class PredicateComposition {
static Predicate<String> p1 = s -> s.contains("bar"), p2 = s -> s.length() < 5, p3 = s -> s.contains("foo"),
p4 = p1.negate().and(p2).or(p3);
public static void main(String[] args) {
Stream.of("bar", "foobar", "foobaz", "fongopuckey").filter(p4).forEach(System.out::println);
}
}
柯里化和部分求值
柯里化意为:将一个多参数的函数,转换为一系列单参数函数。
对于每个级别的箭头级联(Arrow-cascading),在类型声明中包裹了另一个 Function。