本文介绍了 Java8 中的函数式编程方式,结合代码示例详细地讲解了什么是 Lambda 表达式与方法引用。
1. 新方式与旧方式的对比
通常情况下,方法会根据所传递的数据产生不同的结果。如果想让一个方法在每次调用时都有不同的表现呢?如果将代码传递给方法,就可以控制其行为。
以前的做法是,创建一个对象,让它的一个方法包含所需行为,然后将这个对象传递给我们想控制的方法。下面的示例演示了这一点,然后增加了 Java 8 的实现方式:方法引用和 Lambda 表达式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package funcprog;interface Strategy { String approach (String msg) ; } class DefaultStrategy implements Strategy { @Override 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 DefaultStrategy (); this .msg = msg; } void f () { System.out.println(strategy.approach(msg)); } void changeStrategy (Strategy strategy) { this .strategy = strategy; } public static void main (String[] args) { Strategize s = new Strategize ("Hello world" ); s.f(); Strategy[] strategies = { new Strategy () { @Override public String approach (String msg) { return msg.toUpperCase() + "!" ; } }, msg -> msg.substring(0 , 5 ), Unrelated::twice }; for (Strategy newStrategy: strategies) { s.changeStrategy(newStrategy); s.f(); } } }
Strategy
提供了接口,功能是通过其中唯一的 approach()
方法来承载的,通过创建不同的 Strategy
对象,我们可以创建不同的行为。
传统上,我们通过定义一个实现了 Strategy
接口的类来完成这种行为,比如 DefaultStrategy
。更简洁、自然的方式是创建一个匿名内部类,不过这样仍然会存在一定数量的重复代码,而且我们总是要花点功夫才能明白这里是在使用匿名内部类。
Java 8 的 Lambda 表达式突出的特点是用箭头 ->
将参数和函数体分隔开来,箭头右边是从 Lambda 返回的表达式,这和类定义以及匿名内部类实现了同样的效果,但是代码要少得多。
Java 8 的方法引用是用 ::
,左边是类名或对象名,右边是方法名,但是没有参数列表。
2. Lambda表达式
Lambda 表达式是使用尽可能少的语法编写的函数定义。Lambda 表达式产生的是函数,而不是类,在 Java 虚拟机(JVM)上,一切都是类,所以幕后会有各种各样的操作让 Lambda 看起来像函数。
任何 Lambda 表达式的基本语法如下:
参数;
之后跟一个 ->
,可以读作“产生”;
->
后面跟一个方法体。
需要注意以下几个方面:
如果只有一个参数,可以只写这个参数,不写括号,也可以使用括号,尽管这种方式更不常见。
如果有多个参数,将它们放在使用括号包裹起来的参数列表内。
如果没有参数,必须使用括号来指示空的参数列表。
如果方法体只有一行,那么方法体中表达式的结果会自动成为 Lambda 表达式的返回值,不能使用 return
。
如果 Lambda 表达式需要多行代码,则必须将这些代玛行放到 {}
中,这种情况下需要使用 return
从 Lambda 表达式生成一个值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package funcprog;interface Description { String f () ; } interface Body { String f (String str) ; } interface Multi { String f (String str, int x) ; } public class LambdaExpressions { static Description d = () -> "Hello World!" ; static Body b1 = s -> "Hello " + s; static Body b2 = (s) -> "Hello " + s; static Multi m = (s, x) -> { System.out.println("Multi" ); return "Hello " + s + " " + x; }; public static void main (String[] args) { System.out.println(d.f()); System.out.println(b1.f("AsanoSaki" )); System.out.println(b2.f("AsanoSaki" )); System.out.println(m.f("AsanoSaki" , 666 )); } }
递归意味着一个函数调用了自身。在 Java 中也可以编写递归的 Lambda 表达式,但是要注意这个 Lambda 表达式必须被赋值给一个静态变量或一个实例变星,否则会出现编译错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package funcprog;interface Factorial { int f (int n) ; } public class RecursiveFactorial { static Factorial fact; public static void main (String[] args) { fact = n -> n == 0 ? 1 : n * fact.f(n - 1 ); for (int i = 0 ; i < 5 ; i++) System.out.print(fact.f(i) + " " ); } }
请注意,不能在定义的时候像这样来初始化 fact
:
1 static Factorial fact = n -> n == 0 ? 1 : n * fact.f(n - 1 );
尽管这样的期望非常合理,但是对于 Java 编译器而言处理起来太复杂了,所以会产生编译错误。
现在我们再用递归的 Lambda 表达式实现斐波那契数列,这次使用实例变量,用构造器来初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package funcprog;interface Fibonacci { int f (int n) ; } public class RecursiveFibonacci { Fibonacci fib; RecursiveFibonacci() { fib = n -> n == 0 ? 0 : n == 1 ? 1 : fib.f(n - 2 ) + fib.f(n - 1 ); } public static void main (String[] args) { RecursiveFibonacci rf = new RecursiveFibonacci (); for (int i = 0 ; i < 10 ; i++) System.out.print(rf.fib.f(i) + " " ); } }
3. 方法引用
Java 8 方法引用指向的是方法,没有之前 Java 版本的历史包袱,方法引用是用类名或对象名,后面跟 ::
,然后跟方法名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package funcprog;interface Callable { void call (String s) ; } class Print { void print (String s) { System.out.println(s); } } public class MethodReferences { static void hello (String s) { System.out.println("Hello " + s); } static class GoodMorning { void goodMorning (String s) { System.out.println("Good morning " + s); } } static class GoodEvening { static void goodEvening (String s) { System.out.println("Good evening " + s); } } public static void main (String[] args) { Print p = new Print (); Callable c = p::print; c.call("AsanoSaki" ); c = MethodReferences::hello; c.call("AsanoSaki" ); c = new GoodMorning ()::goodMorning; c.call("AsanoSaki" ); c = GoodEvening::goodEvening; c.call("AsanoSaki" ); } }
3.1 Runnable
java.lang
包中的 Runnable
接口也遵从特殊的单方法接口格式,其 run()
方法没有参数,也没有返回值,所以我们可以将 Lambda 表达式或方法引用用作 Runnable
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package funcprog;class RunnableReference { static void f () { System.out.println("RunnableReference::f()" ); } } public class RunnableMethodReference { public static void main (String[] args) { new Thread (new Runnable () { @Override public void run () { System.out.println("Anonymous" ); } }).start(); new Thread ( () -> System.out.println("Lambda" ) ).start(); new Thread (RunnableReference::f).start(); } }
3.2 未绑定方法引用
未绑定方法引用(unbound method reference)指的是尚未关联到某个对象的普通(非静态)方法,对于未绑定引用,必须先提供对象,然后才能使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package funcprog;class A { String f () { return "A::f()" ; } } interface GetStringUnbound { String get () ; } interface GetStringBoundA { String get (A a) ; } public class UnboundMethodReference { public static void main (String[] args) { GetStringBoundA g = A::f; A a = new A (); System.out.println(a.f()); System.out.println(g.get(a)); } }
如果我们按以下方式将 A::f
赋值给 GetStringUnbound
编译器会报错,即使 get()
的签名和 f()
相同。问题在于,这里事实上还涉及另一个(隐藏的)参数:我们的老朋友 this
。如果没有一个可供附着的 A
对象,就无法调用 f()
。因此,A::f
代表的是一个未绑定方法引用,因为它没有绑定到某个对象。
为解决这个冋题,我们需要一个 A
对象,所以我们的接口事实上还需要一个额外的参数,如 GetStringBoundA
中所示,如果将 A::f
赋值给一个 GetStringBoundA
,Java 则会幵心地接受。在未绑定引用的情况下,函数式方法(接口中的单一方法)的签名与方法引用的签名不再完全匹配,这样做有一个很好的理由,那就是我们需要一个对象,让方法在其上调用。
在 g.get(a)
中我们接受了未绑定引用,然后以 A
为参数在其上调用了 get()
,最终以某种方式调用了 a.f()
。Java 知道它必须接受第一个参数,事实上就是 this
,并在它的上面调用该方法。
如果方法有更多参数,只要遵循第一个参数取的是 this
这种模式即可,即对于本例来说第一个参数为 A
即可。
3.3 构造器方法引用
我们也可以捕获对某个构造器的引用,之后通过该引用来调用那个构造器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package funcprog;class Cat { String name; Cat() { name = "Kitty" ; } Cat(String name) { this .name = name; } } interface MakeCatNoArgs { Cat makeCat () ; } interface MakeCatWithArgs { Cat makeCat (String name) ; } public class ConstructorReference { public static void main (String[] args) { MakeCatNoArgs m1 = Cat::new ; MakeCatWithArgs m2 = Cat::new ; Cat c1 = m1.makeCat(); Cat c2 = m2.makeCat("Lucy" ); } }
4. 函数式接口
方法引用和 Lambda 表达式都必须赋值,而这些赋值都需要类型信息,让编译器确保类型的正确性,尤其是 Lambda 表达式,又引入了新的要求,考虑如下代码:
我们看到返回类型必须是 String
,但是 x
是什么类型呢?因为 Lambda 表达式包含了某种形式的类型推断 (编译器推断出类型的某些信息,而不需要程序员显式指定),所以编译器必须能够以某种方式推断出 x
的类型。
下面是第二个示例:
现在 x
和 y
可以是支持 +
操作符的任何类型,包括两种不同的数值类型,或者是一个 String
和某个能够自动转换为 String
的其他类型。
Java 8 引入了包含一组接口的 java.util.function
,这些接口是 Lambda 表达式和方法引用的目标类型,每个接口都只包含一个抽象方法,叫作函数式方法 。当编写接口时,这种函数式方法模式可以使用 @FunctionalInterface
注解来强制实施:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package funcprog;@FunctionalInterface interface FunctionalPrint { void print (String s) ; } @FunctionalInterface interface NotFunctional { void f1 () ; void f2 () ; } public class FunctionalAnnotation { public void hello (String s) { System.out.println("Hello " + s); } public static void main (String[] args) { FunctionalAnnotation f = new FunctionalAnnotation (); FunctionalPrint fp = f::hello; fp.print("AsanoSaki" ); fp = s -> System.out.println("Hi " + s); fp.print("AsanoSaki" ); } }
@FunctionalInterface
注解是可选的,Java 会将 main()
中的 FunctionalPrint
看作函数式接口。在 NotFunctional
接口的定义中我们可以看到 @FunctionalInterface
的作用:如果接口中的方法多于一个,则会产生一条编译错误信息。
现在我们仔细看一下 fp
的定义中发生了什么,FunctionalPrint
定义了接口,然而被赋值给它们的只是方法 hello()
,而不是类,它甚至不是实现了这里定义的某个接口的类中的方法。这是 Java 8 增加的一个小魔法:如果我们将一个方法引用或 Lambda 表达式赋值给某个函数式接口(而且类型可以匹配),那么 Java 会调整这个赋值,使其匹配目标接口。而在底层,Java 编译器会创建一个实现了目标接口的类的实例 ,并将我们的方法引用或 Lambda 表达式包裹在其中。
使用了 @FunctionalInterface
注解的接口也叫作单一抽象方法 (Single Abstract Method,SAM)类型。
4.1 默认目标接口
java.util.function
旨在创建一套足够完备的目标接口,这样一般情况下我们就不需要定义自己的接口了。这一套接口的命名遵循一定的规律,一般来说通过名字就可以了解特定的接口是做什么的,部分接口示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package funcprog;import java.util.function.BiConsumer;import java.util.function.DoubleFunction;import java.util.function.Function;import java.util.function.IntToDoubleFunction;public class FunctionVariants { static Function<Integer, String> f = x -> "Function " + x; static DoubleFunction<String> df = x -> "DoubleFunction " + x; static IntToDoubleFunction itdf = x -> x / 2.0 ; static BiConsumer<Integer, String> bc = (x, s) -> System.out.println("BiConsumer " + x + " " + s); public static void main (String[] args) { System.out.println(f.apply(6 )); System.out.println(df.apply(6 )); System.out.println(itdf.applyAsDouble(6 )); bc.accept(6 , "AsanoSaki" ); } }
4.2 带有更多参数的函数式接口
java.util.function
中的接口毕竟是有限的,如果我们需要有3个参数的函数接口呢?因为那些接口相当直观,所以看一下 Java 库的源代码,然后编写我们自己的接口也很容易:
1 2 3 4 5 6 7 8 9 10 11 12 13 package funcprog;@FunctionalInterface interface TriFunction <T, U, V, R> { R apply (T t, U u, V v) ; } public class TriFunctionTest { public static void main (String[] args) { TriFunction<Integer, Long, Double, Double> tf = (i, j, k) -> i + j + k; System.out.println(tf.apply(1 , 2L , 3D )); } }
5. 高阶函数
高阶函数是一个能接受函数作为参数或能把函数当返回值的函数,有了 Lambda 表达式,在方法中创建并返回一个函数简直不费吹灰之力,要接受并使用函数,方法必须在其参数列表中正确地描述函数类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package funcprog;import java.util.function.Function;public class ProduceFunction { static Function<String, String> produce () { return String::toLowerCase; } static String consume (Function<Integer, String> toStr, int x) { return toStr.apply(x); } public static void main (String[] args) { Function<String, String> toLower = produce(); System.out.println(toLower.apply("Hello World" )); System.out.println(consume(x -> "To String " + x, 6 )); } }
6. 函数组合
函数组合是指将多个函数结合使用,以创建新的函数,这通常被认为是函数式编程的一部分。java.util.function
中的一些接口也包含了支持函数组合的方法,我们以 andThen()
和 compose()
方法为例:
andThen()
:先执行原始操作,再执行方法参数中的操作。
compose()
:先执行方法参数中的操作,再执行原始操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 package funcprog;import java.util.function.Function;public class FunctionComposition { static Function<String, String> f1 = s -> s.substring(0 , 5 ), f2 = s -> new StringBuilder (s).reverse().toString(); public static void main (String[] args) { System.out.println(f1.andThen(f2).apply("Hello World" )); System.out.println(f1.compose(f2).apply("Hello World" )); } }