Java 8 的 Lambda 表达式和流处理(1)Lambda 表达式
- UID
- 1066743
|
Java 8 的 Lambda 表达式和流处理(1)Lambda 表达式
Lambda 表达式当提到 Java 8 的时候,Lambda 表达式总是第一个提到的新特性。Lambda 表达式把函数式编程风格引入到了 Java 平台上,可以极大的提高 Java 开发人员的效率。这也是 Java 社区期待已久的功能,已经有很多的文章和图书讨论过 Lambda 表达式。本文则是基于官方的 JSR 335(Lambda Expressions for the Java Programming Language)来从另外一个角度介绍 Lambda 表达式。
引入 Lambda 表达式的动机我们先从清单 1 中的代码开始谈起。该示例的功能非常简单,只是启动一个线程并输出文本到控制台。虽然该 Java 程序一共有 9 行代码,但真正有价值的只有其中的第 5 行。剩下的代码全部都是为了满足语法要求而必须添加的冗余代码。代码中的第 3 到第 7 行,使用 java.lang.Runnable 接口的实现创建了一个新的 java.lang.Thread 对象,并调用 Thread 对象的 start 方法来启动它。Runnable 接口是通过一个匿名内部类实现的。
清单 1. 传统的启动线程的方式1
2
3
4
5
6
7
8
9
| public class OldThread {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("Hello World!");
}
}).start();
}
}
|
从简化代码的角度出发,第 3 行和第 7 行的 new Runnable() 可以被删除,因为接口类型 Runnable 可以从类 Thread 的构造方法中推断出来。第 4 和第 6 行同样可以被删除,因为方法 run 是接口 Runnable 中的唯一方法。把第 5 行代码作为 run 方法的实现不会出现歧义。把第 3,4,6 和 7 行的代码删除掉之后,就得到了使用 Lambda 表达式的实现方式,如清单 2 所示。只用一行代码就完成了清单 1 中 5 行代码完成的工作。这是令人兴奋的变化。更少的代码意味着更高的开发效率和更低的维护成本。这也是 Lambda 表达式深受欢迎的原因。
清单 2. 使用 Lambda 表 达式启动线程1
2
3
4
5
| public class LambdaThread {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello World!")).start();
}
}
|
简单来说,Lambda 表达式是创建匿名内部类的语法糖(syntax sugar)。在编译器的帮助下,可以让开发人员用更少的代码来完成工作。
函数式接口在对清单 1 的代码进行简化时,我们定义了两个前提条件。第一个前提是要求接口类型,如示例中的 Runnable,可以从当前上下文中推断出来;第二个前提是要求接口中只有一个抽象方法。如果一个接口仅有一个抽象方法(除了来自 Object 的方法之外),它被称为函数式接口(functional interface)。函数式接口的特别之处在于其实例可以通过 Lambda 表达式或方法引用来创建。Java 8 的 java.util.function 包中添加了很多新的函数式接口。如果一个接口被设计为函数式接口,应该添加@FunctionalInterface 注解。编译器会确保该接口确实是函数式接口。当尝试往该接口中添加新的方法时,编译器会报错。
目标类型Lambda 表达式没有类型信息。一个 Lambda 表达式的类型由编译器根据其上下文环境在编译时刻推断得来。举例来说,Lambda 表达式 () -> System.out.println("Hello World!") 可以出现在任何要求一个函数式接口实例的上下文中,只要该函数式接口的唯一方法不接受任何参数,并且返回值是 void。这可能是 Runnable 接口,也可能是来自第三方库或应用代码的其他函数式接口。由上下文环境所确定的类型称为目标类型。Lambda 表达式在不同的上下文环境中可以有不同的类型。类似 Lambda 表达式这样,类型由目标类型确定的表达式称为多态表达式(poly expression)。
Lambda 表达式的语法很灵活。它们的声明方式类似 Java 中的方法,有形式参数列表和主体。参数的类型是可选的。在不指定类型时,由编译器通过上下文环境来推断。Lambda 表达式的主体可以返回值或 void。返回值的类型必须与目标类型相匹配。当 Lambda 表达式的主体抛出异常时,异常的类型必须与目标类型的 throws 声明相匹配。
由于 Lambda 表达式的类型由目标类型确定,在可能出现歧义的情况下,可能有多个类型满足要求,编译器无法独自完成类型推断。这个时候需要对代码进行改写,以帮助编译器完成类型推断。一个常见的做法是显式地把 Lambda 表达式赋值给一个类型确定的变量。另外一种做法是显示的指定类型。
在清单 3 中,函数式接口 A 和 B 分别有方法 a 和 b。两个方法 a 和 b 的类型是相同的。类 UseAB 的 use 方法有两个重载形式,分别接受类 A 和 B 的对象作为参数。在方法 targetType 中,如果直接使用 () -> System.out.println("Use") 来调用 use 方法,会出现编译错误。这是因为编译器无法推断该 Lambda 表达式的类型,类型可能是 A 或 B。这里通过显式的赋值操作为 Lambda 表达式指定了类型 A,从而可以编译通过。
清单 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
28
| public class LambdaTargetType {
@FunctionalInterface
interface A {
void a();
}
@FunctionalInterface
interface B {
void b();
}
class UseAB {
void use(A a) {
System.out.println("Use A");
}
void use(B b) {
System.out.println("Use B");
}
}
void targetType() {
UseAB useAB = new UseAB();
A a = () -> System.out.println("Use");
useAB.use(a);
}
}
|
名称解析在 Lambda 表达式的主体中,经常需要引用来自包围它的上下文环境中的变量。Lambda 表达式使用一个简单的策略来处理主体中的名称解析问题。Lambda 表达式并没有引入新的命名域(scope)。Lambda 表达式中的名称与其所在上下文环境在同一个词法域中。Lambda 表达式在执行时,就相当于是在包围它的代码中。在 Lambda 表达式中的 this 也与包围它的代码中的含义相同。在清单 4 中,Lambda 表达式的主体中引用了来自包围它的上下文环境中的变量 name。
清单 4. Lambda 表 达式中的名称解析1
2
3
4
| public void run() {
String name = "Alex";
new Thread(() -> System.out.println("Hello, " + name)).start();
}
|
需要注意的是,可以在 Lambda 表达式中引用的变量必须是声明为 final 或是实际上 final(effectively final)的。实际上 final 的意思是变量虽然没有声明为 final,但是在初始化之后没有被赋值。因此变量的值没有改变。 |
|
|
|
|
|