Lambda表达式 与 方法引用
Lambda表达式
Lambda 表达式是使用最小可能语法编写的函数定义:
-
Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。
-
Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。
我们在 Strategize.java 中看到了一个 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()
我们从三个接口开始,每个接口都有一个单独的方法(很快就会理解它的重要性)。但是,每个方法都有不同数量的参数,以便演示 Lambda 表达式语法。
任何 Lambda 表达式的基本语法是:
-
参数。
-
接着
->
,可视为“产出”。 -
->
之后的内容都是方法体。
-
[1] 当只用一个参数,可以不需要括号
()
。 然而,这是一个特例。 -
[2] 正常情况使用括号
()
包裹参数。 为了保持一致性,也可以使用括号()
包裹单个参数,虽然这种情况并不常见。 -
[3] 如果没有参数,则必须使用括号
()
表示空参数列表。 -
[4] 对于多个参数,将参数列表放在括号
()
中。
到目前为止,所有 Lambda 表达式方法体都是单行。 该表达式的结果自动成为 Lambda 表达式的返回值,在此处使用 return 关键字是非法的。 这是 Lambda 表达式简化相应语法的另一种方式。
[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。 在这种情况下,就需要使用 return。
Lambda 表达式通常比匿名内部类产生更易读的代码,因此我们将在本书中尽可能使用它们。
递归
递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例变量或静态变量,否则会出现编译时错误。 我们将为每个案例创建一个示例。
这两个示例都需要一个接受 int 型参数并生成 int 的接口:
// functional/IntCall.java
interface IntCall {
int call(int arg);
}
整数 n 的阶乘将所有小于或等于 n 的正整数相乘。 阶乘函数是一个常见的递归示例:
// 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
是一个静态变量。 注意使用三元 if-else。 递归函数将一直调用自己,直到 i == 0
。所有递归函数都有“停止条件”,否则将无限递归并产生异常。
我们可以将 Fibonacci
序列用递归的 Lambda 表达式来实现,这次使用实例变量:
// 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
序列中的最后两个元素求和来产生下一个元素。
方法引用
Java 8 方法引用没有历史包袱。方法引用组成:类名或对象名,后面跟 ::
[^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!
[1] 我们从单一方法接口开始(同样,你很快就会了解到这一点的重要性)。
[2] show()
的签名(参数类型和返回类型)符合 Callable 的 call()
的签名。
[3] hello()
也符合 call()
的签名。
[4] help()
也符合,它是静态内部类中的非静态方法。
[5] assist()
是静态内部类中的静态方法。
[6] 我们将 Describe 对象的方法引用赋值给 Callable ,它没有 show()
方法,而是 call()
方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 Callable 的 call()
方法的签名。
[7] 我们现在可以通过调用 call()
来调用 show()
,因为 Java 将 call()
映射到 show()
。
[8] 这是一个静态方法引用。
[9] 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为绑定方法引用。
[10] 最后,获取静态内部类中静态方法的引用与 [8] 中通过外部类引用相似。
上例只是简短的介绍,我们很快就能看到方法引用的所有不同形式。
Runnable接口
Runnable 接口自 1.0 版以来一直在 Java 中,因此不需要导入。它也符合特殊的单方法接口格式:它的方法 run()
不带参数,也没有返回值。因此,我们可以使用 Lambda 表达式和方法引用作为 Runnable:
// 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()
Thread 对象将 Runnable 作为其构造函数参数,并具有会调用 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()
截止目前,我们看到了与对应接口签名相同的方法引用。 在 [1],我们尝试把 X
的 f()
方法引用赋值给 MakeString。结果即使 make()
与 f()
具有相同的签名,编译也会报“invalid method reference”(无效方法引用)错误。 这是因为实际上还有另一个隐藏的参数:我们的老朋友 this
。 你不能在没有 X
对象的前提下调用 f()
。 因此,X :: f
表示未绑定的方法引用,因为它尚未“绑定”到对象。
要解决这个问题,我们需要一个 X
对象,所以我们的接口实际上需要一个额外的参数,如上例中的 TransformX。 如果将 X :: f
赋值给 TransformX,在 Java 中是允许的。我们必须做第二个心理调整——使用未绑定的引用时,函数式方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。 原因是:你需要一个对象来调用方法。
[2] 的结果有点像脑筋急转弯。我拿到未绑定的方法引用,并且调用它的transform()
方法,将一个X类的对象传递给它,最后使得 x.f()
以某种方式被调用。Java知道它必须拿到第一个参数,该参数实际就是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');
}
}
需要指出的是,我将类命名为 This,并将函数式方法的第一个参数命名为 athis,但你在生产级代码中应该使用其他名字,以防止混淆。
构造函数引用
你还可以捕获构造函数的引用,然后通过引用调用该构造函数。
// 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);
}
}
Dog 有三个构造函数,函数式接口内的 make()
方法反映了构造函数参数列表( make()
方法名称可以不同)。
注意我们如何对 [1],[2] 和 [3] 中的每一个使用 Dog :: new
。 这三个构造函数只有一个相同名称::: new
,但在每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数。
编译器知道调用函数式方法(本例中为 make()
)就相当于调用构造函数。
上一篇: JDK 1.8 新特性记录
下一篇: java8时间和lambda表达式用法