12、第十三章函数式编程

函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式方法引用 (Method References) 允许你以函数式编程。

OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。

Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。这对 Java 来说是一个巨大的改进。因为这允许你编写更简洁明了,易于理解的代码。

没有泛型 Lambda,所以 Lambda 在 Java 中并非一等公民。

Lambda表达式

Lambda 表达式是使用最小可能语法编写的函数定义:

1、 Lambda表达式产生函数,而不是类在JVM(JavaVirtualMachine,Java虚拟机)上,一切都是一个类,因此在幕后执行各种操作使Lambda看起来像函数——但作为程序员,你可以高兴地假装它们“只是函数”;
2、 Lambda语法尽可能少,这正是为了使Lambda易于编写和使用;

任何Lambda 表达式的基本语法是:

1、 参数;
2、 接着->,可视为“产出”;
3、 ->之后的内容都是方法体;

递归

递归函数是一个自我调用的函数。可以编写递归的 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 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。这主要是因为基本类型会产生一小部分接口。 如果你了解命名模式,顾名思义就能知道特定接口的作用。
以下是基本命名准则:

1、 如果只处理对象而非基本类型,名称则为FunctionConsumerPredicate等参数类型通过泛型添加;
2、 如果接收的参数是基本类型,则由名称的第一部分表示,如LongConsumerDoubleFunctionIntPredicate等,但基本Supplier类型例外;
3、 如果返回值为基本类型,则用To表示,如ToLongFunction<T>IntToLongFunction
4、 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用UnaryOperator,两个参数使用BinaryOperator
5、 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate);
6、 如果接收的两个参数类型不同,则名称中有一个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。