13-Functional-Programming
第十三章 函数式编程
函数式编程语言操纵代码片段就像操作数据一样容易。 虽然
Java 不是函数式语言,但Java 8 Lambda 表达式和方法引用(Method References) 允许你以函数式编程。
在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们虽然知道编译器,但编译器生成的代码很低效,比手工编码的汇编程序多很多字节,仅仅想到这一点,人们还是选择汇编语言。
通常,为了使程序能在有限的内存上运行,在程序运行时,程序员通过修改内存中的代码,使程序可以执行不同的操作,用这种方式来节省代码空间。这种技术被称为自修改代码 (self-modifying code
随着内存和处理器变得更便宜、更快。
随着硬件越来越便宜,程序的规模和复杂性都在增长。这一切只是让程序工作变得困难。我们想方设法使代码更加一致和易懂。使用纯粹的自修改代码造成的结果就是:我们很难确定程序在做什么。它也难以测试:除非你想一点点测试输出,代码转换和修改等等过程?
然而,使用代码以某种方式操纵其他代码的想法也很有趣,只要能保证它更安全。从代码创建,维护和可靠性的角度来看,这个想法非常吸引人。我们不用从头开始编写大量代码,而是从易于理解、充分测试及可靠的现有小块开始,最后将它们组合在一起以创建新代码。难道这不会让我们更有效率,同时创造更健壮的代码吗?
这就是函数式编程(FP)的意义所在。通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用。
你也可以这样想:
OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。
纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素
更好的是
需要提醒大家的是,函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:为“并行编程”
新旧对比
通常,传递给方法的数据不同,结果不同。如果我们希望方法在调用时行为不同,该怎么做呢?结论是:只要能将代码传递给方法,我们就可以控制它的行为。此前,我们通过在方法中创建包含所需行为的对象,然后将该对象传递给我们想要控制的方法来完成此操作。下面我们用传统形式和
// functional/Strategize.java
interface Strategy {
String approach(String msg);
}
class Soft implements Strategy {
public String approach(String msg) {
return msg.toLowerCase() + "?";
}
}
class Unrelated {
static String twice(String msg) {
return msg + " " + msg;
}
}
public class Strategize {
Strategy strategy;
String msg;
Strategize(String msg) {
strategy = new Soft(); // [1]
this.msg = msg;
}
void communicate() {
System.out.println(strategy.approach(msg));
}
void changeStrategy(Strategy strategy) {
this.strategy = strategy;
}
public static void main(String[] args) {
Strategy[] strategies = {
new Strategy() { // [2]
public String approach(String msg) {
return msg.toUpperCase() + "!";
}
},
msg -> msg.substring(0, 5), // [3]
Unrelated::twice // [4]
};
Strategize s = new Strategize("Hello there");
s.communicate();
for(Strategy newStrategy : strategies) {
s.changeStrategy(newStrategy); // [5]
s.communicate(); // [6]
}
}
}
输出结果
hello there?
HELLO THERE!
Hello
Hello there Hello there
approach()
方法来承载函数式功能。通过创建不同的
我们一般通过创建一个实现
-
[1] 在 Strategize 中,你可以看到 Soft 作为默认策略,在构造函数中赋值。 -
[2] 一种较为简洁且更加自然的方法是创建一个匿名内部类。即便如此,仍有相当数量的冗余代码。你总需要仔细观察后才会发现 : “哦,我明白了,原来这里使用了匿名内部类。 ” -
[3] Java 8 的Lambda 表达式,其参数和函数体被箭头->
分隔开。箭头右侧是从Lambda 返回的表达式。它与单独定义类和采用匿名内部类是等价的,但代码少得多。 -
[4] Java 8 的方法引用,它以::
为特征。::
的左边是类或对象的名称,::
的右边是方法的名称,但是没有参数列表。 -
[5] 在使用默认的 Soft 策略之后,我们逐步遍历数组中的所有 Strategy ,并通过调用changeStrategy()
方法将每个Strategy 传入变量 s
中。 -
[6] 现在,每次调用 communicate()
都会产生不同的行为,具体取决于此刻正在使用的策略代码对象。我们传递的是行为,而并不仅仅是数据。3
在
Lambda 表达式
-
Lambda 表达式产生函数,而不是类。 虽然在JVM (Java Virtual Machine,Java 虚拟机)上,一切都是类,但是幕后有各种操作执行让Lambda 看起来像函数 —— 作为程序员,你可以高兴地假装它们“就是函数”。 -
Lambda 语法尽可能少,这正是为了使Lambda 易于编写和使用。
我们在
// functional/LambdaExpressions.java
interface Description {
String brief();
}
interface Body {
String detailed(String head);
}
interface Multi {
String twoArg(String head, Double d);
}
public class LambdaExpressions {
static Body bod = h -> h + " No Parens!"; // [1]
static Body bod2 = (h) -> h + " More details"; // [2]
static Description desc = () -> "Short info"; // [3]
static Multi mult = (h, n) -> h + n; // [4]
static Description moreLines = () -> { // [5]
System.out.println("moreLines()");
return "from moreLines()";
};
public static void main(String[] args) {
System.out.println(bod.detailed("Oh!"));
System.out.println(bod2.detailed("Hi!"));
System.out.println(desc.brief());
System.out.println(mult.twoArg("Pi! ", 3.14159));
System.out.println(moreLines.brief());
}
}
输出结果:
Oh! No Parens!
Hi! More details
Short info
Pi! 3.14159
moreLines()
from moreLines()
我们从三个接口开始,每个接口都有一个单独的方法(很快就会理解它的重要性
任何
-
参数。
-
接着
->
,可视为“产出”。 -
->
之后的内容都是方法体。
-
[1] 当只用一个参数,可以不需要括号 ()
。 然而,这是一个特例。 -
[2] 正常情况使用括号 ()
包裹参数。 为了保持一致性,也可以使用括号()
包裹单个参数,虽然这种情况并不常见。 -
[3] 如果没有参数,则必须使用括号 ()
表示空参数列表。 -
[4] 对于多个参数,将参数列表放在括号 ()
中。
到目前为止,所有
递归
递归函数是一个自我调用的函数。可以编写递归的
这两个示例都需要一个接受
// functional/IntCall.java
interface IntCall {
int call(int arg);
}
整数
// functional/RecursiveFactorial.java
public class RecursiveFactorial {
static IntCall fact;
public static void main(String[] args) {
fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
for(int i = 0; i <= 10; i++)
System.out.println(fact.call(i));
}
}
输出结果:
1
1
2
6
24
120
720
5040
40320
362880
3628800
这里,fact
是一个静态变量。 注意使用三元i == 0
。所有递归函数都有“停止条件”,否则将无限递归并产生异常。
我们可以将 Fibonacci
序列用递归的
// functional/RecursiveFibonacci.java
public class RecursiveFibonacci {
IntCall fib;
RecursiveFibonacci() {
fib = n -> n == 0 ? 0 :
n == 1 ? 1 :
fib.call(n - 1) + fib.call(n - 2);
}
int fibonacci(int n) { return fib.call(n); }
public static void main(String[] args) {
RecursiveFibonacci rf = new RecursiveFibonacci();
for(int i = 0; i <= 10; i++)
System.out.println(rf.fibonacci(i));
}
}
输出结果:
0
1
1
2
3
5
8
13
21
34
55
将 Fibonacci
序列中的最后两个元素求和来产生下一个元素。
方法引用
::
4,然后跟方法名称。
// functional/MethodReferences.java
import java.util.*;
interface Callable { // [1]
void call(String s);
}
class Describe {
void show(String msg) { // [2]
System.out.println(msg);
}
}
public class MethodReferences {
static void hello(String name) { // [3]
System.out.println("Hello, " + name);
}
static class Description {
String about;
Description(String desc) { about = desc; }
void help(String msg) { // [4]
System.out.println(about + " " + msg);
}
}
static class Helper {
static void assist(String msg) { // [5]
System.out.println(msg);
}
}
public static void main(String[] args) {
Describe d = new Describe();
Callable c = d::show; // [6]
c.call("call()"); // [7]
c = MethodReferences::hello; // [8]
c.call("Bob");
c = new Description("valuable")::help; // [9]
c.call("information");
c = Helper::assist; // [10]
c.call("Help!");
}
}
输出结果:
call()
Hello, Bob
valuable information
Help!
show()
call()
的签名。
hello()
call()
的签名。
help()
assist()
show()
方法,而是 call()
方法。 但是,call()
方法的签名。
call()
来调用 show()
,因为call()
映射到 show()
。
上例只是简短的介绍,我们很快就能看到方法引用的所有不同形式。
Runnable 接口
run()
不带参数,也没有返回值。因此,我们可以使用
// functional/RunnableMethodReference.java
// 方法引用与 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();
}
}
输出结果:
Anonymous
lambda
Go::go()
run()
的方法 start()
。 注意这里只有匿名内部类才要求显式声明 run()
方法。
未绑定的方法引用
未绑定的方法引用是指没有关联对象的普通(非静态)方法。 使用未绑定的引用时,我们必须先提供对象:
// functional/UnboundMethodReference.java
// 没有方法引用的对象
class X {
String f() { return "X::f()"; }
}
interface MakeString {
String make();
}
interface TransformX {
String transform(X x);
}
public class UnboundMethodReference {
public static void main(String[] args) {
// MakeString ms = X::f; // [1]
TransformX sp = X::f;
X x = new X();
System.out.println(sp.transform(x)); // [2]
System.out.println(x.f()); // 同等效果
}
}
输出结果:
X::f()
X::f()
到目前为止,我们已经见过了方法引用和对应接口的签名(参数类型和返回类型)一致的几个赋值例子。 在X
的 f()
方法引用赋值给make()
与 f()
具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 问题在于,这里其实还需要另一个隐藏参数参与:我们的老朋友 this
。 你不能在没有 X
对象的前提下调用 f()
。 因此,X :: f
表示未绑定的方法引用,因为它尚未“绑定”到对象。
要解决这个问题,我们需要一个 X
对象,因此我们的接口实际上需要一个额外的参数,正如在X :: f
赋值给
transform()
方法,将一个x.f()
以某种方式被调用。this
对象,然后对此调用方法。
如果你的方法有更多个参数,就以第一个参数接受this
的模式来处理。
// functional/MultiUnbound.java
// 未绑定的方法与多参数的结合运用
class This {
void two(int i, double d) {}
void three(int i, double d, String s) {}
void four(int i, double d, String s, char 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');
}
}
需要指出的是,我将类命名为
构造函数引用
你还可以捕获构造函数的引用,然后通过引用调用该构造函数。
// functional/CtorReference.java
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; }
}
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();
Dog d1 = m1a.make("Comet");
Dog d2 = m2a.make("Ralph", 4);
}
}
make()
方法反映了构造函数参数列表( make()
方法名称可以不同
注意我们如何对Dog :: new
。 这三个构造函数只有一个相同名称::: new
,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。
编译器知道调用函数式方法(本例中为 make()
)就相当于调用构造函数。
函数式接口
方法引用和
x -> x.toString()
我们清楚这里返回类型必须是x
是什么类型呢?
x
的类型。
下面是第二个代码示例:
(x, y) -> x + y
现在 x
和 y
可以是任何支持 +
运算符连接的数据类型,可以是两个不同的数值类型或者是 一个x
和 y
的确切类型以生成正确的代码。
该问题也适用于方法引用。 假设你要传递 System.out :: println
到你正在编写的方法 ,你怎么知道传递给方法的参数的类型?
为了解决这个问题,java.util.function
包。它包含一组接口,这些接口是
在编写接口时,可以使用 @FunctionalInterface
注解强制执行此“函数式方法”模式:
// functional/FunctionalAnnotation.java
@FunctionalInterface
interface Functional {
String goodbye(String arg);
}
interface FunctionalNoAnn {
String goodbye(String arg);
}
/*
@FunctionalInterface
interface NotFunctional {
String goodbye(String arg);
String hello(String arg);
}
产生错误信息:
NotFunctional is not a functional interface
multiple non-overriding abstract methods
found in interface NotFunctional
*/
public class FunctionalAnnotation {
public String goodbye(String arg) {
return "Goodbye, " + arg;
}
public static void main(String[] args) {
FunctionalAnnotation fa =
new FunctionalAnnotation();
Functional f = fa::goodbye;
FunctionalNoAnn fna = fa::goodbye;
// Functional fac = fa; // Incompatible
Functional fl = a -> "Goodbye, " + a;
FunctionalNoAnn fnal = a -> "Goodbye, " + a;
}
}
@FunctionalInterface
注解是可选的main()
中把NotFunctional
的定义中可看出@FunctionalInterface
的作用:当接口中抽象方法多于一个时产生编译期错误。
仔细观察在定义 f
和 fna
时发生了什么。 Functional
和 FunctionalNoAnn
声明了是接口,然而被赋值的只是方法 goodbye()
。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法。这是添加到
虽然 FunctionalAnnotation
确实符合 Functional
模型,但是fac
定义的那样,将 FunctionalAnnotation
直接赋值给 Functional
,因为 FunctionalAnnotation
并没有显式地去实现 Functional
接口。唯一的惊喜是,
java.util.function
包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口。主要因为基本类型的存在,导致预定义的接口数量有少许增加。 如果你了解命名模式,顾名思义就能知道特定接口的作用。
以下是基本命名准则:
-
如果只处理对象而非基本类型,名称则为
Function
,Consumer
,Predicate
等。参数类型通过泛型添加。 -
如果接收的参数是基本类型,则由名称的第一部分表示,如
LongConsumer
,DoubleFunction
,IntPredicate
等,但返回基本类型的Supplier
接口例外。 -
如果返回值为基本类型,则用
To
表示,如ToLongFunction <T>
和IntToLongFunction
。 -
如果返回值类型与参数类型相同,则是一个
Operator
:单个参数使用UnaryOperator
,两个参数使用BinaryOperator
。 -
如果接收参数并返回一个布尔值,则是一个 谓词
( Predicate
) 。 -
如果接收的两个参数类型不同,则名称中有一个
Bi
。
下表描述了 java.util.function
中的目标类型(包括例外情况
特征 | 函数式方法名 | 示例 |
---|---|---|
无参数; 无返回值 |
Runnable (java.lang) run() |
Runnable |
无参数; 返回类型任意 |
Supplier get() getAs类型() |
Supplier<T> BooleanSupplier IntSupplier LongSupplier DoubleSupplier |
无参数; 返回类型任意 |
Callable (java.util.concurrent) call() |
Callable<V> |
无返回值 |
Consumer accept() |
Consumer<T> IntConsumer LongConsumer DoubleConsumer |
BiConsumer accept() |
BiConsumer<T,U> |
|
第一个参数是 引用; 第二个参数是 基本类型 |
accept() |
ObjIntConsumer<T> ObjLongConsumer<T> ObjDoubleConsumer<T> |
返回类型不同 |
Function apply() applyAs类型() |
Function<T,R> IntFunction <R> LongFunction<R> DoubleFunction <R> ToIntFunction <T> ToLongFunction<T> ToDoubleFunction<T> IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
返回类型相同 |
UnaryOperator apply() |
UnaryOperator<T> IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
返回类型相同 |
BinaryOperator apply() |
BinaryOperator<T> IntBinaryOperator LongBinaryOperator DoubleBinaryOperator |
返回整型 |
Comparator (java.util) compare() |
Comparator<T> |
返回布尔型 |
Predicate test() |
Predicate<T> BiPredicate<T,U> IntPredicate LongPredicate DoublePredicate |
参数基本类型; 返回基本类型 |
类型applyAs类型() |
IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction |
类型不同 |
BiFunction<T,U,R> BiConsumer<T,U> BiPredicate<T,U> ToIntBiFunction<T,U> ToLongBiFunction<T,U> ToDoubleBiFunction<T> |
此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出你所需要的函数式接口。
可以看出,在创建 java.util.function
时,设计者们做出了一些选择。
例如,为什么没有 IntComparator
,LongComparator
和 DoubleComparator
呢?有 BooleanSupplier
却没有其他表示BiConsumer
却没有用于BiConsumers
变体(我理解他们为什么放弃这些接口
你还可以看到基本类型给
下面枚举了基于
// functional/FunctionVariants.java
import java.util.function.*;
class Foo {}
class Bar {
Foo f;
Bar(Foo f) { this.f = f; }
}
class IBaz {
int i;
IBaz(int i) {
this.i = i;
}
}
class LBaz {
long l;
LBaz(long l) {
this.l = l;
}
}
class DBaz {
double d;
DBaz(double d) {
this.d = d;
}
}
public class FunctionVariants {
static Function<Foo,Bar> f1 = f -> new Bar(f);
static IntFunction<IBaz> f2 = i -> new IBaz(i);
static LongFunction<LBaz> f3 = l -> new LBaz(l);
static DoubleFunction<DBaz> f4 = d -> new DBaz(d);
static ToIntFunction<IBaz> f5 = ib -> ib.i;
static ToLongFunction<LBaz> f6 = lb -> lb.l;
static ToDoubleFunction<DBaz> f7 = db -> db.d;
static IntToLongFunction f8 = i -> i;
static IntToDoubleFunction f9 = i -> i;
static LongToIntFunction f10 = l -> (int)l;
static LongToDoubleFunction f11 = l -> l;
static DoubleToIntFunction f12 = d -> (int)d;
static DoubleToLongFunction f13 = d -> (long)d;
public static void main(String[] args) {
Bar b = f1.apply(new Foo());
IBaz ib = f2.apply(11);
LBaz lb = f3.apply(11);
DBaz db = f4.apply(11);
int i = f5.applyAsInt(ib);
long l = f6.applyAsLong(lb);
double d = f7.applyAsDouble(db);
l = f8.applyAsLong(12);
d = f9.applyAsDouble(12);
i = f10.applyAsInt(12);
d = f11.applyAsDouble(12);
i = f12.applyAsInt(13.0);
l = f13.applyAsLong(13.0);
}
}
这些
main()
中的每个测试都显示了 Function
接口中不同类型的 apply()
方法。 每个都产生一个与其关联的
方法引用有自己的小魔法:
/ functional/MethodConversion.java
import java.util.function.*;
class In1 {}
class In2 {}
public class MethodConversion {
static void accept(In1 i1, In2 i2) {
System.out.println("accept()");
}
static void someOtherName(In1 i1, In2 i2) {
System.out.println("someOtherName()");
}
public static void main(String[] args) {
BiConsumer<In1,In2> bic;
bic = MethodConversion::accept;
bic.accept(new In1(), new In2());
bic = MethodConversion::someOtherName;
// bic.someOtherName(new In1(), new In2()); // Nope
bic.accept(new In1(), new In2());
}
}
输出结果:
accept()
someOtherName()
查看 BiConsumer
的文档,你会看到它的函数式方法为 accept()
。 的确,如果我们将方法命名为 accept()
,它就可以作为方法引用。 但是我们也可用不同的名称,比如 someOtherName()
。只要参数类型、返回类型与 BiConsumer
的 accept()
相同即可。
因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 accept()
现在我们来看看,将方法引用应用于基于类的函数式接口(即那些不包含基本类型的函数式接口
// functional/ClassFunctionals.java
import java.util.*;
import java.util.function.*;
class AA {}
class BB {}
class CC {}
public class ClassFunctionals {
static AA f1() { return new AA(); }
static int f2(AA aa1, AA aa2) { return 1; }
static void f3(AA aa) {}
static void f4(AA aa, BB bb) {}
static CC f5(AA aa) { return new CC(); }
static CC f6(AA aa, BB bb) { return new CC(); }
static boolean f7(AA aa) { return true; }
static boolean f8(AA aa, BB bb) { return true; }
static AA f9(AA aa) { return new AA(); }
static AA f10(AA aa1, AA aa2) { return new AA(); }
public static void main(String[] args) {
Supplier<AA> s = ClassFunctionals::f1;
s.get();
Comparator<AA> c = ClassFunctionals::f2;
c.compare(new AA(), new AA());
Consumer<AA> cons = ClassFunctionals::f3;
cons.accept(new AA());
BiConsumer<AA,BB> bicons = ClassFunctionals::f4;
bicons.accept(new AA(), new BB());
Function<AA,CC> f = ClassFunctionals::f5;
CC cc = f.apply(new AA());
BiFunction<AA,BB,CC> bif = ClassFunctionals::f6;
cc = bif.apply(new AA(), new BB());
Predicate<AA> p = ClassFunctionals::f7;
boolean result = p.test(new AA());
BiPredicate<AA,BB> bip = ClassFunctionals::f8;
result = bip.test(new AA(), new BB());
UnaryOperator<AA> uo = ClassFunctionals::f9;
AA aa = uo.apply(new AA());
BinaryOperator<AA> bo = ClassFunctionals::f10;
aa = bo.apply(new AA(), new AA());
}
}
请注意,每个方法名称都是随意的(如 f1()
,f2()
等get()
、compare()
、accept()
、apply()
和 test()
。
多参数函数式接口
java.util.functional
中的接口是有限的。比如有 BiFunction
,但也仅此而已。 如果需要三参数函数的接口怎么办? 其实这些接口非常简单,很容易查看
// functional/TriFunction.java
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
简单测试,验证它是否有效:
// functional/TriFunctionTest.java
public class TriFunctionTest {
static int f(int i, long l, double d) { return 99; }
public static void main(String[] args) {
TriFunction<Integer, Long, Double, Integer> tf =
TriFunctionTest::f;
tf = (i, l, d) -> 12;
}
}
这里我们同时测试了方法引用和
缺少基本类型的函数
让我们重温一下 BiConsumer
,看看我们将如何创建各种缺失的预定义组合,涉及
// functional/BiConsumerPermutations.java
import java.util.function.*;
public class BiConsumerPermutations {
static BiConsumer<Integer, Double> bicid = (i, d) ->
System.out.format("%d, %f%n", i, d);
static BiConsumer<Double, Integer> bicdi = (d, i) ->
System.out.format("%d, %f%n", i, d);
static BiConsumer<Integer, Long> bicil = (i, l) ->
System.out.format("%d, %d%n", i, l);
public static void main(String[] args) {
bicid.accept(47, 11.34);
bicdi.accept(22.45, 92);
bicil.accept(1, 11L);
}
}
输出结果:
47, 11.340000
92, 22.450000
1, 11
这里使用 System.out.format()
来显示。它类似于 System.out.println()
但提供了更多的显示选项。 这里,%f
表示我将 n
作为浮点值给出,%d
表示 n
是一个整数值。 这其中可以包含空格,输入 %n
会换行 — 当然使用传统的 \n
也能换行,但 %n
是自动跨平台的,这是使用 format()
的另一个原因。
上例只是简单使用了合适的包装类型,而装箱和拆箱负责它与基本类型之间的来回转换。 又比如,我们可以将包装类型和Function
一起使用,而不去用各种针对基本类型的预定义接口。代码示例:
// functional/FunctionWithWrapped.java
import java.util.function.*;
public class FunctionWithWrapped {
public static void main(String[] args) {
Function<Integer, Double> fid = i -> (double)i;
IntToDoubleFunction fid2 = i -> i;
}
}
如果没有强制转换,则会收到错误消息
@FunctionalInterface
public interface IntToDoubleFunction {
double applyAsDouble(int value);
}
因为我们可以简单地写 Function <Integer,Double>
并产生正常的结果,所以用基本类型(IntToDoubleFunction
)的唯一理由是可以避免传递参数和返回结果过程中的自动拆装箱,进而提升性能。
似乎是考虑到使用频率,某些函数类型并没有预定义。
当然,如果因为缺少针对基本类型的函数式接口造成了性能问题,你可以轻松编写自己的接口( 参考
高阶函数
这个名字可能听起来令人生畏,但是:高阶函数(Higher-order Function)只是一个消费或产生函数的函数。
我们先来看看如何产生一个函数:
// functional/ProduceFunction.java
import java.util.function.*;
interface
FuncSS extends Function<String, String> {} // [1]
public class ProduceFunction {
static FuncSS produce() {
return s -> s.toLowerCase(); // [2]
}
public static void main(String[] args) {
FuncSS f = produce();
System.out.println(f.apply("YELLING"));
}
}
输出结果:
yelling
这里,produce()
是高阶函数。
要消费一个函数,消费函数需要在参数列表正确地描述函数类型。代码示例:
// functional/ConsumeFunction.java
import java.util.function.*;
class One {}
class Two {}
public class ConsumeFunction {
static Two consume(Function<One,Two> onetwo) {
return onetwo.apply(new One());
}
public static void main(String[] args) {
Two two = consume(one -> new Two());
}
}
当基于消费函数生成新函数时,事情就变得相当有趣了。代码示例如下:
// functional/TransformFunction.java
import java.util.function.*;
class I {
@Override
public String toString() { return "I"; }
}
class O {
@Override
public String toString() { return "O"; }
}
public class TransformFunction {
static Function<I,O> transform(Function<I,O> in) {
return in.andThen(o -> {
System.out.println(o);
return o;
});
}
public static void main(String[] args) {
Function<I,O> f2 = transform(i -> {
System.out.println(i);
return new O();
});
O o = f2.apply(new I());
}
}
输出结果:
I
O
在这里,transform()
生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。
这里使用到了 Function
接口中名为 andThen()
的默认方法,该方法专门用于操作函数。 顾名思义,在调用 in
函数之后调用 andThen()
(还有个 compose()
方法,它在 in
函数之前应用新函数andThen()
函数,我们只需将该函数作为参数传递。 transform()
产生的是一个新函数,它将 in
的动作与 andThen()
参数的动作结合起来。
闭包
在上一节的 ProduceFunction.java
中,我们从方法中返回
闭包(Closure)一词总结了这些问题。 它非常重要,利用闭包可以轻松生成函数。
考虑一个更复杂的
首先,下列方法返回一个函数,该函数访问对象字段和方法参数:
// functional/Closure1.java
import java.util.function.*;
public class Closure1 {
int i;
IntSupplier makeFun(int x) {
return () -> x + i++;
}
}
但是,仔细考虑一下,i
的这种用法并非是个大难题,因为对象很可能在你调用 makeFun()
之后就存在了——实际上,垃圾收集器几乎肯定会保留以这种方式被绑定到现存函数的对象5。当然,如果你对同一个对象多次调用 makeFun()
,你最终会得到多个函数,它们共享 i
的存储空间:
// functional/SharedStorage.java
import java.util.function.*;
public class SharedStorage {
public static void main(String[] args) {
Closure1 c1 = new Closure1();
IntSupplier f1 = c1.makeFun(0);
IntSupplier f2 = c1.makeFun(0);
IntSupplier f3 = c1.makeFun(0);
System.out.println(f1.getAsInt());
System.out.println(f2.getAsInt());
System.out.println(f3.getAsInt());
}
}
输出结果:
0
1
2
每次调用 getAsInt()
都会增加 i
,表明存储是共享的。
如果 i
是 makeFun()
的局部变量怎么办? 在正常情况下,当 makeFun()
完成时 i
就消失。 但它仍可以编译:
// functional/Closure2.java
import java.util.function.*;
public class Closure2 {
IntSupplier makeFun(int x) {
int i = 0;
return () -> x + i;
}
}
由 makeFun()
返回的 IntSupplier
“关住了” i
和 x
,因此即使makeFun()
已执行完毕,当你调用返回的函数时i
和 x
仍然有效,而不是像正常情况下那样在 makeFun()
执行后 i
和x
就消失了。 但请注意,我没有像 Closure1.java
那样递增 i
,因为会产生编译时错误。代码示例:
// functional/Closure3.java
// {WillNotCompile}
import java.util.function.*;
public class Closure3 {
IntSupplier makeFun(int x) {
int i = 0;
// x++ 和 i++ 都会报错:
return () -> x++ + i++;
}
}
x
和 i
的操作都犯了同样的错误:被final
或者是等同 final
效果的。
如果使用 final
修饰 x
和 i
,就不能再递增它们的值了。代码示例:
// functional/Closure4.java
import java.util.function.*;
public class Closure4 {
IntSupplier makeFun(final int x) {
final int i = 0;
return () -> x + i;
}
}
那么为什么在 Closure2.java
中, x
和 i
非 final
却可以运行呢?
这就叫做等同final
的,但是因变量值没被改变过而实际有了 final
同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是 final
的。
如果 x
和 i
的值在方法中的其他位置发生改变(但不在返回的函数内部
// functional/Closure5.java
// {无法编译成功}
import java.util.function.*;
public class Closure5 {
IntSupplier makeFun(int x) {
int i = 0;
i++;
x++;
return () -> x + i;
}
}
等同final
效果的,只是没有明确说明。
在闭包中,在使用 x
和 i
之前,通过将它们赋值给 final
修饰的变量,我们解决了 Closure5.java
中遇到的问题。代码示例:
// functional/Closure6.java
import java.util.function.*;
public class Closure6 {
IntSupplier makeFun(int x) {
int i = 0;
i++;
x++;
final int iFinal = i;
final int xFinal = x;
return () -> xFinal + iFinal;
}
}
上例中 iFinal
和 xFinal
的值在赋值后并没有改变过,因此在这里使用 final
是多余的。
如果改用包装类型会是什么情况呢?我们可以把int
类型改为Integer
类型研究一下:
// functional/Closure7.java
// {无法编译成功}
import java.util.function.*;
public class Closure7 {
IntSupplier makeFun(int x) {
Integer i = 0;
i = i + 1;
return () -> x + i;
}
}
编译器非常聪明地识别到变量 i
的值被更改过。 包装类型可能是被特殊处理了,我们再尝试下
// functional/Closure8.java
import java.util.*;
import java.util.function.*;
public class Closure8 {
Supplier<List<Integer>> makeFun() {
final List<Integer> ai = new ArrayList<>();
ai.add(1);
return () -> ai;
}
public static void main(String[] args) {
Closure8 c7 = new Closure8();
List<Integer>
l1 = c7.makeFun().get(),
l2 = c7.makeFun().get();
System.out.println(l1);
System.out.println(l2);
l1.add(42);
l2.add(96);
System.out.println(l1);
System.out.println(l2);
}
}
输出结果:
[1]
[1]
[1, 42]
[1, 96]
可以看到,这次一切正常。我们改变了makeFun()
时,其实都会创建并返回一个全新而非共享的 ArrayList
。也就是说,每个闭包都有自己独立的 ArrayList
,它们之间互不干扰。
请注意我已经声明 ai
是 final
的了。尽管在这个例子中你可以去掉 final
并得到相同的结果(试试吧final
关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。
我们来看看 Closure7.java
和 Closure8.java
之间的区别。我们看到:在 Closure7.java
中变量 i
有过重新赋值。 也许这就是触发等同
// functional/Closure9.java
// {无法编译成功}
import java.util.*;
import java.util.function.*;
public class Closure9 {
Supplier<List<Integer>> makeFun() {
List<Integer> ai = new ArrayList<>();
ai = new ArrayList<>(); // Reassignment
return () -> ai;
}
}
上例,重新赋值引用会触发错误消息。如果只修改指向的对象则没问题,只要没有其他人获得对该对象的引用(这意味着你有多个实体可以修改对象,此时事情会变得非常混乱
让我们回顾一下 Closure1.java
。那么现在问题来了:为什么变量 i
被修改编译器却没有报错呢。 它既不是 final
的,也不是等同i
是外部类的成员,所以这样做肯定是安全的(除非你正在创建共享可变内存的多个函数Closure3.java
的错误消息是专门针对局部变量的。因此,规则并非只是 “在final
的或等同
作为闭包的内部类
我们可以使用匿名内部类重写之前的例子
// functional/AnonymousClosure.java
import java.util.function.*;
public class AnonymousClosure {
IntSupplier makeFun(int x) {
int i = 0;
// 同样规则的应用:
// i++; // 非等同 final 效果
// x++; // 同上
return new IntSupplier() {
public int getAsInt() { return x + i; }
};
}
}
实际上只要有内部类,就会有闭包(x
和 i
必须被明确声明为 final
。在
函数组合
函数组合(Function Composition)意为“多个函数组合成新函数”。它通常是函数式编程的基本组成部分。在前面的 TransformFunction.java
类中,就有一个使用 andThen()
的函数组合示例。一些 java.util.function
接口中包含支持函数组合的方法 7。
组合方法 | 支持接口 |
---|---|
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 |
下例使用了 Function
里的 compose()
和 andThen()
。代码示例:
// functional/FunctionComposition.java
import java.util.function.*;
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"));
}
}
输出结果:
AFTER ALL AMBULANCES
_fter _ll _mbul_nces
这里我们重点看正在创建的新函数 f4
。它调用 apply()
的方式与常规几乎无异8。
当 f1
获得字符串时,它已经被f2
剥离了前三个字符。这是因为 compose(f2)
表示 f2
的调用发生在 f1
之前。
下例是 谓词Predicate
// functional/PredicateComposition.java
import java.util.function.*;
import java.util.stream.*;
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);
}
}
输出结果:
foobar
foobaz
p4
获取到了所有谓词Predicate
bar
且长度小于foo
,则结果为 true
。
正因它产生如此清晰的语法,我在主方法中采用了一些小技巧,并借用了下一章的内容。首先,我创建了一个字符串对象的流,然后将每个对象传递给 filter()
操作。 filter()
使用 p4
的谓词来确定对象的去留。最后我们使用 forEach()
将 println
方法引用应用在每个留存的对象上。
从输出结果我们可以看到 p4
的工作流程:任何带有 "foo"
的字符串都得以保留,即使它的长度大于"fongopuckey"
因长度超出且不包含 foo
而被丢弃。
柯里化和部分求值
柯里化(Currying)的名称来自于其发明者之一
// functional/CurryingAndPartials.java
import java.util.function.*;
public class CurryingAndPartials {
// 未柯里化:
static String uncurried(String a, String b) {
return a + b;
}
public static void main(String[] args) {
// 柯里化的函数:
Function<String, Function<String, String>> sum =
a -> b -> a + b; // [1]
System.out.println(uncurried("Hi ", "Ho"));
Function<String, String>
hi = sum.apply("Hi "); // [2]
System.out.println(hi.apply("Ho"));
// 部分应用:
Function<String, String> sumHi =
sum.apply("Hup ");
System.out.println(sumHi.apply("Ho"));
System.out.println(sumHi.apply("Hey"));
}
}
输出结果:
Hi Ho
Hi Ho
Hup Ho
Hup Hey
我们可以通过继续添加层级来柯里化一个三参数函数:
// functional/Curry3Args.java
import java.util.function.*;
public class Curry3Args {
public static void main(String[] args) {
Function<String,
Function<String,
Function<String, String>>> sum =
a -> b -> c -> a + b + c;
Function<String,
Function<String, String>> hi =
sum.apply("Hi ");
Function<String, String> ho =
hi.apply("Ho ");
System.out.println(ho.apply("Hup"));
}
}
输出结果:
Hi Ho Hup
对于每一级的箭头级联(Arrow-cascading
处理基本类型和装箱时,请使用适当的函数式接口:
// functional/CurriedIntAdd.java
import java.util.function.*;
public class CurriedIntAdd {
public static void main(String[] args) {
IntFunction<IntUnaryOperator>
curriedIntAdd = a -> b -> a + b;
IntUnaryOperator add4 = curriedIntAdd.apply(4);
System.out.println(add4.applyAsInt(5));
}
}
输出结果:
9
可以在互联网上找到更多的柯里化示例。通常它们是用
纯函数式编程
只要多加练习,用没有函数式支持的语言也可以写出纯函数式程序(即使是final
的,同时你的所有方法和函数没有副作用。因为
这种情况下,我们可以借助第三方工具9,但使用Scala
或 Clojure
之类的语言。
本章小结
这些特性满足了很大一部分的、羡慕
但是,
当你遇到学习困难时,请记住通过
-
功能粘贴在一起的方法的确有点与众不同,但它仍不失为一个库。 ↩︎
-
有时函数式语言将其描述为“代码即数据”。 ↩︎
-
这个语法来自
C++ 。 ↩︎ -
我还没有验证过这种说法。 ↩︎
-
接口能够支持方法的原因是它们是
Java 8 默认方法,你将在下一章中了解到。 ↩︎ -
一些语言,如
Python ,允许像调用其他函数一样调用组合函数。但这是Java ,所以我们要量力而行。 ↩︎ -
例如,Immutables 和 Mutability Detector。 ↩︎