Java Tutorials Lambda表达式 翻译

时间:2022-03-05 20:17:27

原文

http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

 

Lambda 表达式

匿名类有一个问题,当你的匿名类实现非常简单,比如接口只包含一个方法,

那么匿名类的语法会显得比较笨重且不清晰。在这些时候,你通常是想将一个

功能作为参数传给另一个方法,比如当有人按下一个按钮时应该做什么处理。

Lambda表达式使你可以做到这样,将功能作为方法的参数对待,或者说,将

代码做为数据传递。

 

前面的章节,匿名类,展示了如何实现一个基类但不需要给他一个类名。尽管

这通常比一个命名类更为简洁,但是对于只有一个方法的类,即使是匿名类也

显得有一点不简洁且笨重。Lambda表达式让你可以跟简洁地表现单方法类的实例。

 

这个章节有以下小节:

1.Lambda表达式的理想用例 

  步骤1:创建一些方法用于检索集合中符合某特性的成员

  步骤2:创建更通用的检索方法

  步骤3:在一个本地类中指定检索的逻辑代码

  步骤4:在一个匿名类中指定检索的逻辑代码

  步骤5:用Lambda表达式指定检索的逻辑代码

  步骤6:用Lambda表达式来实现标准的函数式接口

  步骤7:在你的程序中使用Lambda表达式

  步骤8:使用泛型更可扩展

  步骤9:使用可以接受Lambda表达式作为参数的聚合操作

2.GUI程序中的Lambda表达式

3.Lambda表达式的语法

4.在封闭的作用域中访问本地变量

5.目标类型

  目标类型与方法参数

6.序列化

 

1.Lambda表达式的理想用例

假设你在创建一个社交软件。你想要实现这样一个功能,管理员可以做任何操作,

比如发送消息给社交网络上满足一定条件的所有用户。下面的表格详细描述了这个

用例。

 

字段 描述
名字 对选中的用户做操作
主要实施者 管理员
前提条件 管理员已经登录
确认项目 对满足条件的用户做了操作
方案

1.管理员指定要对哪些用户做操作

2.管理员指定要对那些选中的用户做的操作

3.管理员点击提交按钮

4.系统找到所有符合要求的用户

5.系统对所有符合的用户执行操作

扩展 1a.管理员有预览哪些用户符合条件的选项,在他确定要做什么操作或者选择提交之前。
发生频率 一天很多次

 

假设社交软件上的用户用下面的Person类来实现:

 1 public class Person {
2
3 public enum Sex {
4 MALE, FEMALE
5 }
6
7 String name;
8 LocalDate birthday;
9 Sex gender;
10 String emailAddress;
11
12 public int getAge() {
13 // ...
14 }
15
16 public void printPerson() {
17 // ...
18 }
19 }

假设社交软件上的用户被保存在一个List<Person>对象中。

这个章节从一个比较傻的方法开始实现这个用例。首先用内部类和匿名类来优化,

最后用一个效率且简洁的Lambda表达式来实现。查看这个章节中叫RosterTest的例子

的代码。

 

步骤1 创建一些方法用于检索集合中符合某特性的成员

一个简单的实现是创建几个发那个方法,每个方法检索符合一个特性的成员,比如

性别或年龄。下面的方法打印出年龄高于某一数值的成员:

1 public static void printPersonsOlderThan(List<Person> roster, int age) {
2 for (Person p : roster) {
3 if (p.getAge() >= age) {
4 p.printPerson();
5 }
6 }
7 }

注意:一个List是一个有序的Collection。一个Collection是一个组合多个成员到一个单位中的类。

Collection被用来存取,操作,传输集合数据。

这个方法可能让你的软件很难用,软件可能不能正常工作当引入了更新(比如新的数据类型),

想象这种情况,你更新了你的软件,更改了Person类的结构比如让它包含不同的成员变量,

也可能是一个类使用不一样的数据类型或者算法来记录和计算年龄。你不得不重写很多你的

API来适应这些变化。另外,这个方法还有不必要的限制,比如,你要是想打印比这个年龄小

的成员该怎么办。

 

步骤2  创建更通用的检索方法

下面的方法比printPersonsOlderThan更通用,它打印一个年龄区间内的成员。

 

1 public static void printPersonsWithinAgeRange(
2 List<Person> roster, int low, int high) {
3 for (Person p : roster) {
4 if (low <= p.getAge() && p.getAge() < high) {
5 p.printPerson();
6 }
7 }
8 }

假如你想要打印某个性别的成员或者性别与年龄组合为条件的成员该怎么办呢?

假如你决定要改变Person类,加入其他的属性,比如关系状态或者地理位置呢?

尽管这个方法比printPersonsOlderThan更通用,但是为每个可能的检索去新建特定

的方法还是会使代码变得容易失效。你可以将指定对象的检索逻辑代码放到不同

的类中。

 

步骤3 在一个本地类中指定检索的逻辑代码

下面的方法打印那些符合你指定的检索逻辑的成员信息。

1 public static void printPersons(
2 List<Person> roster, CheckPerson tester) {
3 for (Person p : roster) {
4 if (tester.test(p)) {
5 p.printPerson();
6 }
7 }
8 }

这个方法调用tester.test方法检查每个参数List roster中的Person对象是否满足检索条件,

如果tester.test返回true,则Person的printPersons方法会被调用。

你实现了CheckPerson接口来指定检索逻辑。

interface CheckPerson {
boolean test(Person p);
}

下面的类实现了CheckPerson接口,实现了方法test的逻辑。这个方法过滤符合美国选择性服务的

成员:但它的参数Person是男性并且年龄在18到25之间则返回true。

class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge()
>= 18 &&
p.getAge()
<= 25;
}
}

要使用这个类的时候,你新建一个它的实例,然后调用它的printPerson方法。

printPersons(
roster,
new CheckPersonEligibleForSelectiveService());

竟然这个方法比前面的好一点——你不需要重写方法当你改变了Person的结构——你仍然有多

余的代码:对于软件中每个你想要做的检索都需要一个新的接口和一个内部类。因为CheckPersonEligibleForSelectiveService 实现了接口,你可以使用一个匿名类代替内部类,

在每个检索需要声明一个新类的地方。

 

步骤4 在一个匿名类中指定检索的逻辑代码

下面调用printPersons方法时的参数之一是一个匿名类,它过滤满足美国选择性服务条件的

成员:男性并且年龄在18到25之间。

printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);

这个方法减少了需要的代码量,因为你不用为每个检索新建一个类。但是匿名类的语法不灵活,

限定CheckPerson接口只能包含一个方法。这种情况下,下面的章节描述了你可以使用Lambda

表达式来代替匿名类。

 

步骤5 用Lambda表达式指定检索的逻辑代码

CheckPerson接口是一个函数式接口。一个函数式接口指那些只包含一个抽象方法的接口。

(一个函数式接口可以包含一个或多个默认方法或静态方法)因为函数式接口只包含一个

抽象方法,所有你可以忽略方法名当你实现它的时候。代替匿名类表达式,你使用Lambda

表达式来做这件事。在下面的方法调用中它被高亮了。

printPersons(
roster,
(Person p)
-> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25

);

查看Syntax of Lambda Expressions来了解如何定义Lambda表达式。

你可以使用一个标准的函数式接口来代替CheckPerson接口,来进一步减少需要的代码。

 

步骤6 用Lambda表达式来实现标准的函数式接口

重新考虑CheckPerson接口:

interface CheckPerson {
boolean test(Person p);
}

这是一个非常简单的接口。它是一个函数式接口因为它只包含一个抽象方法。这个方法有一个

参数并返回一个boolean值。这个方法是如此简单,可能都不值得去为它定义一个方法。所以,

JDK定义了一些标准的函数接口,你可以在java.util.function包中找到。

比如,你可以使用Predicate<T>接口来代替CheckPerson。

这个接口包含了方法boolean test(T t):

interface Predicate<T> {
boolean test(T t);
}

Predicate<T>接口是一个泛型接口的例子。泛型(比如泛型接口)用三角括号(<>)来指定一

个或多个类型参数。这个接口只有一个类型参数,T。当你使用一个具体的类型声明或者实例化

泛型的时候,你将一个类型作为了参数。比如下面的例子,参数化类型Predicate<Person>

interface Predicate<Person> {
boolean test(Person t);
}

参数类型包含一个有着相同的返回值和参数的方法比如CheckPerson.boolean test(Person p)。

所以你可以像下面的方法演示的那样用Predicate<T> 代替CheckPerson:

public static void printPersonsWithPredicate(
List
<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

结果是,和步骤3中一样,可以用下面的方法调用来获得可以有选择性服务的成员:

printPersonsWithPredicate(
roster,
p
-> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);

在方法中使用Lambda表达式不是唯一可以使用它的地方。下面的步骤中有其他使用Lambda表

达式的建议。

 

步骤7 在你的程序中使用Lambda表达式

重新考虑方法printPersonsWithPredicate,思考还有哪里可以使用Lambda表达式。

public static void printPersonsWithPredicate(
List
<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

这个方法检查每个存在于List参数roster中的Person实例,确认是否符合Predicate型参数tester

所定义的条件。如果Person实例满足条件,这个Person实例的printPersons方法会被调用。

 

不是去调用printPerson方法,你可以对满足tester所定义的逻辑的成员执行其他的操作。你可以

用Lambda表达式来指定操作。假设你需要一个Lambda表达式,更printPerson类似,有一个参数

(Person类型)并返回void。记住,要使用lambda表达式,你需要去实现一个函数式接口。在

这个例子中,你需要一个函数式接口,它包含一个有一个Person参数并返回void的抽象方法。

 Consumer<T> 接口包含方法void accept(T t),满足这些特性。下面的方法用一个调用accept的

 Consumer<Person> 实例来代替调用p.printPerson()。

public static void processPersons(
List
<Person> roster,
Predicate
<Person> tester,
Consumer
<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}

所以像步骤3一样,可以用lambda表达式改写为下面的方法调用。

processPersons(
roster,
p
-> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p
-> p.printPerson()
);

如果你不止想打印成员信息,而是更多的操作。假设你想要检查成员的账号或者获取联络信息。

这时候,你需要一个包含有返回值的抽象方法的函数式接口。Function<T,R>接口包含方法

R apply(T t)。下面的方法从参数mapper获取数据,并用参数block对其执行操作。

public static void processPersonsWithFunction(
List
<Person> roster,
Predicate
<Person> tester,
Function
<Person, String> mapper,
Consumer
<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data
= mapper.apply(p);
block.accept(data);
}
}
}

下面的方法获取每个roster中满足选择性服务的成员的email地址并打印它。

processPersonsWithFunction(
roster,
p
-> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p
-> p.getEmailAddress(),
email
-> System.out.println(email)
);

 

步骤8 使用泛型更可扩展

再思考下方法processPersonsWithFunction。下面是一个泛型版本,接收一个包含任何数据类

型的集合作为参数。

public static <X, Y> void processElements(
Iterable
<X> source,
Predicate
<X> tester,
Function
<X, Y> mapper,
Consumer
<Y> block) {
for (X p : source) {
if (tester.test(p)) {
Y data
= mapper.apply(p);
block.accept(data);
}
}
}

要打印满足选择性服务的成员的email地址,像这样调用processElements方法。

processElements(
roster,
p
-> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p
-> p.getEmailAddress(),
email
-> System.out.println(email)
);

这个方法执行了以下操作。

1.从集合source中获得对象资源。在例子中,从集合roster中获得Person类型的资源。注意集合

roster,它的类型是List,同时也是Iterable的对象。

2.过滤满足 Predicate 类型 tester的对象。例子中,Predicate对象是一个lambda表达式指定哪些

成员符合选择性服务。

3.用Function类型mapper将过滤后的对象映射到一个值。例子中,Function对象是一个lambda表

达式返回每个成员的email地址。

4.对映射后的结果值执行Consumer类型block中定义的操作。例子中,Consumer对象是一个

lambda表达式打印Function对象返回的email地址。

你可以将这些操作中的任意一个换成聚合操作。

 

步骤9 使用可以接受Lambda表达式作为参数的聚合操作

下面的例子用聚合操作来打印roster中满足选择性服务的成员的email地址。

roster
.stream()
.filter(
p
-> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p
-> p.getEmailAddress())
.forEach(email
-> System.out.println(email));

下面的表格展示了processElements 中每个操作与其对应的聚合操作。

processElements操作 聚合操作                    
获取数据 Stream<E> stream()
过滤符合Predicate 的对象 Stream<T> filter(Predicate<? super T> predicate)
根据Function映射对象到另一个值 <R> Stream<R> map(Function<? super T,? extends R> mapper)
执行Consumer中的操作 void forEach(Consumer<? super T> action)

操作filter,map和foreach都是聚合操作。聚合操作从stream中操作对象,而不是直接从集合中(

这是为什么第一个调用的方法是stream)。一个stream是一个元素的队列。和集合不同,它不是

一个存储元素的数据结构。stream通过管道(pipeline)从source中比如集合中搬运数据。管道

(pipeline)是一个stream操作的队列,比如例子中的 filtermap-forEach。另外,聚合操作一般

以lambda表达式为参数,从而使你可以定制它的行为。

 

2.GUI程序中的Lambda表达式

要处理一个图形用户接口软件的事件,比如键盘操作,鼠标操作和滚动栏操作,你一般会创建事件

handler,handler通常会调用一个特定接口的实现。事件handler经常是一个函数式接口,他们应该

只有一个方法。

在JavaFX例子中,你可以将匿名类替换成lambda表达式。

        btn.setOnAction(new EventHandler<ActionEvent>() {

@Override
public void handle(ActionEvent event) {
System.out.println(
"Hello World!"
);
}

});

方法btn.setOnAction定义了当你按了btn代表的按钮时会发生什么。这个方法需要一个

EventHandler<ActionEvent>.类型的对象作为参数。EventHandler<ActionEvent>接口只

有一个方法void handle(T event)。这是个函数式接口。所以你可以用下面的lambda表

达式代替它。

       btn.setOnAction(
event
-> System.out.println("Hello World!")
);

 

3.Lambda表达式的语法

一个lambda表达式由以下组成

  •  一个用括号包围并用逗号隔开的参数列表。CheckPerson.test方法包含一个参数,p,

  代表一个Person类型的实例。

  注意:在lambda表达式中,你可以省略数据类型,另外,你可以省略括号当你只有一个参数

  的时候。比如下面 是lambda表达式也是合法的

p -> p.getGender() == Person.Sex.MALE 
&& p.getAge() >= 18
&& p.getAge() <= 25
  • 箭头符号 ->
  • 单个表达式或者一个语句块组成的body。例子中是以下表达式
p.getGender() == Person.Sex.MALE 
&& p.getAge() >= 18
&& p.getAge() <= 25

  如果你定义一个单句表达式,那么Java运行环境会执行这个表达式,然后返回它的值。

       或者你可以使用一个return语句。

p -> {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}

   return语句不是表达式,在lambda表达式中,你必须将语句用花括号({})括起来。然而

  你不需要将一个void方法括起来。比如下面的lambda表达式是合法的。

email -> System.out.println(email)

注意lambda表达式看起来像是一个方法定义,你可以把lambda表达式看成是匿名方法,没有

名字的方法。

 

下面的例子Calculator是一个多参数的lambda表达式的例子。

public class Calculator {

interface IntegerMath {
int operation(int a, int b);
}

public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}

public static void main(String... args) {

Calculator myApp
= new Calculator();
IntegerMath addition
= (a, b) -> a + b;
IntegerMath subtraction
= (a, b) -> a - b;
System.out.println(
"40 + 2 = " +
myApp.operateBinary(
40, 2, addition));
System.out.println(
"20 - 10 = " +
myApp.operateBinary(
20, 10, subtraction));
}
}

方法operateBinary 对两个整数做数学运算。操作本身是一个IntegerMath的实例。例子用lambda

表达式定义了两个操作,addition 和 subtraction。例子的输出如下。

40 + 2 = 42
20 - 10 = 10

 

4.在封闭的作用域中访问本地变量

与内部类和匿名类一样,lambda表达式可以获取变量,它们对封闭作用域中的本地变量有着相同

的权限。然而,与内部类和匿名类不同,lambda表达式没有Shadowing特性(不同作用域,变量

重名,ShadowTest.this.x  代表上层ShadowTest类的scope中的x)。lambda表达式是单词作用域

的。这表示它不从父类中继承名字,也不会引入新的作用域。Lambda表达式中的声明,被解读为

它们在封闭环境中。下面的例子LambdaScopeTest演示了这个特性。

import java.util.function.Consumer;

public class LambdaScopeTest {

public int x = 0;

class FirstLevel {

public int x = 1;

void methodInFirstLevel(int x) {

// The following statement causes the compiler to generate
// the error "local variables referenced from a lambda expression
// must be final or effectively final" in statement A:
//
// x = 99;

Consumer
<Integer> myConsumer = (y) ->
{
System.out.println(
"x = " + x); // Statement A
System.out.println("y = " + y);
System.out.println(
"this.x = " + this.x);
System.out.println(
"LambdaScopeTest.this.x = " +
LambdaScopeTest.
this.x);
};

myConsumer.accept(x);

}
}

public static void main(String... args) {
LambdaScopeTest st
= new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl
= st.new FirstLevel();
fl.methodInFirstLevel(
23);
}
}

例子生成以下输出:

x = 23
y
= 23
this.x = 1
LambdaScopeTest.
this.x = 0

如果你用x代替y来声明lambda表达式myConsumer, 那么编译器会产生错误

Consumer<Integer> myConsumer = (x) -> {
// ...
}

编译器会生成 "variable x is already defined in method methodInFirstLevel(int)"的错误,因为

lambda表达式不会引入新的作用域。所以,你可以直接访问作用域中的字段,方法,本地变量。

比如,直接访问方法methodInFirstLevel的参数x。访问当前类中的变量,使用关键字this。

例子中,this.x代表FirstLevel.x这个成员变量。

然而,与内部类和匿名类一样,lambda表达式只能访问闭合块中的final或者effectively final。

(一个变量在初始化之后,值再也没有被改变,那么它是effectively final的)

比如,假如你在methodInFirstLevel 声明语句后面紧跟着加上下面的赋值语句。

void methodInFirstLevel(int x) {
x
= 99;
// ...
}

由于这个赋值语句,变量FirstLevel.x不再effectively final 。所以,Java编译器在lambda

表达式myCosumer想要访问FirstLevel.x变量时发出类似于 "local variables referenced

from a lambda expression must be final or effectively final" 的错误。

 

5.目标类型

如何确定lambda表达式的类型。回顾选择18-25之间男性的lambda表达式。

p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25

 

这个lambda表达式被使用在两个方法中。

 

当Java运行环境调用printPersons方法,期待一个CheckPerson类型的数据,所以,lambda
表达式是CheckPerson类型。然而当调用printPersonWithPredicate时,期待Predicate<Person>
类型的数据,所以,lambda表达式是Predicate<Person>类型的。这些方法期待的类型叫做目标类型。
要确定一个lambda表达式的类型,java编译器用上下文的目标类型或者找到的lambda表达式的内容。
你只能在java编译器可以确定目标类型的情况下使用lambda表达式。

 

Variable declarations  变量定义

 

 

Assignments   赋值

 

 

Return statements  返回语句

 

 

Array initializers  数组初始化

 

 

Method or constructor arguments  方法或构造函数参数

 

 

Lambda expression bodies  lambda表达式主体

 

 

Conditional expressions, ?: 三元演算式

 

 

Cast expressions  类型转换表达式

 

目标类型与方法参数

对于方法参数,java编译器根据两个语言特性来确定目标类型:重载解析与参数类型推断。

思考下面来年改革函数是接口(java.lang.Runable和java.util.concurrent.Callable<V>)

public interface Runnable {
void run();
}

public interface Callable<V> {
V call();
}

Runnable方法没有返回值而Callable<V>有。

假设你像下面这样重载了方法。

void invoke(Runnable r) {
r.run();
}

<T> T invoke(Callable<T> c) {
return c.call();
}

 

下面的语句中哪个方法会被调用?

String s = invoke(() -> "done");

方法invoke(Callable<V>)会被调用,因为它有返回值,而Runnable没有。在这个例子中,

lambda表达式() -> "done"的类型是Callable<V>。

 

6.序列化

但lambda表达式的返回值和参数都是可序列化的,它本身也可以序列化。

但是和内部类一样,强烈建议不要序列化lambda表达式。

 


 

syntax ˈsinˌtaks 句法
unwieldy ˌənˈwēldē 笨重
functionality ˌfəNGkSHəˈnalətē 功能
concise kənˈsīs 简洁
excessive ikˈsesiv 过多
cumbersome ˈkəmbərsəm 笨重
compactly kəm'pæktli 简洁地
approach əˈprōCH 途径,用途
characteristic ˌkariktəˈristik 特性
generalized ˈdʒenrəlaɪzd 广泛的,一般性的
specify ˈspesəˌfī 指定
criteria kraɪ'tɪərɪə (批评、判断等的)标准,准则( criterion的名词复数 )
precondition ˈprikənˈdiʃən 前提,先决条件
postcondition 后置条件
scenario sɪˈneəri:ˌəʊ (行动的)方案; 剧情概要; 分镜头剧本;
potentially pəˈtenʃəlɪ 可能地
brittle ˈbritl 易碎的; 难以相处的,尖刻暴躁的; 冷淡的; 声音尖利的;
algorithm ˈalgəˌriT͟Həm 算法
accommodate əˈkäməˌdāt 容纳,适应,迁就
consequently ˌkwentlē 所以