本文是对Java SE 8: Lambda Quick Start的翻译,供个人学习Lambda表达式之用。成文日期:2018-11-01,所有以引文出现的文字为笔者阅读过程中添加,非原文内容。
强烈建议CET4以上的读者阅读原文。

概述

目标

本文旨在介绍在 Java SE 8 中引进的 Lambda 表达式。

阅读时间

大约 1 小时

简介

Lambda 表达式是 Java SE 8 新引入的重要特性。它提供了通过一个表达式表示指代接口的简洁方法。Lambda 表达式也在遍历、过滤、以及读取数据方面优化了Collection类的使用。此外,新的并发特性提升了多核环境下的程序性能。

总结一下就是三个优点:1.更简洁的匿名类写法,2.更便利的集合类处理,3.多核环境性能优化。

本文首先提供了对 Lambda 表达式的介绍说明,然后介绍匿名内部函数,接着对函数借口和新的 Lambda 语法进行讨论。然后列举出一些常用使用方法模版。

下节通过对一个搜索场景的讨论,引申出 Lambda 表达式如何优化这种功能实现。然后介绍了一些java.util.function包里面常用的函数接口,如PredicateFunction,并举了一些栗子。

文末介绍如何通过 Lambda 优化集合类使用。

提供所有样例源码。

硬件和软件需求

以下是硬件和软件需求列表

  • JDK 8
  • NetBeans 7.4

可以用IDEA代替NetBeans

(运行样例的)先决条件

要运行样例,你的电脑上必须安装有JDK 8和NetBeans 7.4或更高版本。你可以在the main Lambda site找到下载链接。或者可以直接使用下面的链接。

注意: 安装包适用于所有主流操作系统。本文基于 Linux Mint 13(Ubuntu/Debian)完成。

安装完成JDK8和NetBeans后。将它们的bin文件夹添加到你的环境变量PATH中。

注意: 本文最后更新于 2013 年十二月。

竟然是5年以前的文章了…

背景知识

匿名内部类(Anonymous Inner Class)

在 Java 中,匿名内部类提供一种实现仅使用一次的类的途径。例如,在标准的 Swing 或者 JavaFX 应用中,需要用一系列的 handler 来处理键盘和鼠标事件。除了为每一个事件单独写一个事件处理类(该类需要实现 ActionListener 接口)这种方式,你可以用以下代码来实现类似需求

1
2
3
4
5
6
JButton testButton = new JButton("Test Button");
testButton.addActionListener(new ActionListener(){
@Override public void actionPerformed(ActionEvent ae){
System.out.println("Click Detected by Anon Class");
}
});

除此之外,你需要为每一个事件都写下类似的代码,它们都需要实现ActionListener接口。通过上述匿名内部类的写法,代码似乎变得容易阅读一些。然而这种写法很不优雅,因为有太多无用的模版代码,我们需要的其实只是“在函数里我要做什么事”这个信息。

函数接口(Functional Interfaces)

定义ActionListener的类代码如下

1
2
3
4
5
6
7
8
package java.awt.event;
import java.util.EventListener;

public interface ActionListener extends EventListener {

public void actionPerformed(ActionEvent e);

}

ActionListener的梨子是一个只含有一个函数声明的接口。在 Java SE 8 中,这种模式的接口被称为“函数接口”。

注意: 这种接口之前被称为“Single Abstrace Method type”(单一抽象方法类型)

在 Java 中使用匿名类实现函数接口是一种常见写法。除了EventListener类以外,像RunnableComparator这样的接口也有一样的模式。因此,函数式接口是我们使用 Lambda 表达式的一个重要原因。

Lambda 表达式语法

Lambda 可以把匿名内部类的代码从 5 行精简到 1 行,极大的缩减了代码的冗余。如下这种“水平的”解决方案解决了匿名内部类带来的“垂直的”问题。

这里“水平”和“垂直”是指代码块在页面中呈现的形式

一个 Lambda 表达式由三部分构成

Argument List Arrow Token Body
(int x, int y) -> x + y

这三个部分可以翻译为“参数列表”、“箭头标志”、“函数体”,不过下文仍然以英文称呼,为了加深记忆。

Body 部分可以是一个简单的表达式(expression)或者代码块(statement block)。在上面的模版中,body 中进行了简单的计算和返回。在代码块格式中,body 呈现形式与普通函数写法一样,同样需要一个 return 语句来返回结果给匿名函数的调用者。breakcontinue关键字在最外层是禁止使用的,不过如果代码里存在循环(loops),则可以使用。如果 body 需要返回一个结果值,每一条控制路径都必需有值返回,或者抛出异常。

看一下这些栗子:

1
2
3
4
5
(int x, int y) -> x + y

() -> 42

(String s) -> { System.out.println(s); }

第一个表达式读取两个整型参数,xy,然后返回x+y的结果。第二个表达式不需要参数,使用“表达式”格式的返回结果,值为42。第三个表达式读取字符串参数,然后将它打印在控制台,不返回任何结果。

有了上面的基础知识,我们再来看一些样例。

Lambda 样例(Lambda Examples)

这里是一些使用到上面提到的栗子的常见用法。

Runnable Lambda

你可以用 Lambda 来写一个实现了 Runnable 接口的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RunnableTest {
public static void main(String[] args) {

System.out.println("=== RunnableTest ===");

// Anonymous Runnable
Runnable r1 = new Runnable(){
@Override
public void run(){
System.out.println("Hello world one!");
}
};

// Lambda Runnable
Runnable r2 = () -> System.out.println("Hello world two!");

// Run em!
r1.run();
r2.run();
}
}

在两种场景里,请注意到接口不需要读取参数,也没有返回值。Runnable的 Lambda 表达式使用了代码块模式,将 5 行代码浓缩为 1 行。

“代码块”应为原作者笔误,实际上是“表达式”格式。

Comparator Lambda

Comparator类在 Java 中用于给集合排序。在以下的例子中,一个由Person对象构成的ArrayList被按照surName进行排序。以下是Person类的成员变量

1
2
3
4
5
6
7
8
public class Person {
private String givenName;
private String surName;
private int age;
private Gender gender;
private String eMail;
private String phone;
private String address;

以下代码分别使用了匿名内部类和 Lambda 表达式两种方式生成Comparator进行排序。

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
public class ComparatorTest {

public static void main(String[] args) {

List<Person> personList = Person.createShortList();

// Sort with Inner Class
Collections.sort(personList, new Comparator<Person>(){
public int compare(Person p1, Person p2){
return p1.getSurName().compareTo(p2.getSurName());
}
});

System.out.println("=== Sorted Asc SurName ===");
for(Person p:personList){
p.printName();
}

// Use Lambda instead

// Print Asc
System.out.println("=== Sorted Asc SurName ===");
Collections.sort(personList, (Person p1, Person p2) -> p1.getSurName().compareTo(p2.getSurName()));

for(Person p:personList){
p.printName();
}

// Print Desc
System.out.println("=== Sorted Desc SurName ===");
Collections.sort(personList, (p1, p2) -> p2.getSurName().compareTo(p1.getSurName()));

for(Person p:personList){
p.printName();
}

}
}

由于 Markdown 不支持代码行数表示,这里的行数请自行体会。

17 - 21 行的排序语句可以被 32 行的 Lambda 表达式精简。注意到第一个 Lambda 表达式声明了传进来的参数类型。第二个 Lambda 表达式则省略了类型声明。Lambda 支持“target typing”(类型自动匹配?),意味着它可以自动从上下文中获取到对象类型信息。因为我们将Comparator应用于泛型集合类(personList),编译器能够自动判断参数类型为Person

Listener Lambda

最后我们来看一下ActionListener的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ListenerTest {
public static void main(String[] args) {

JButton testButton = new JButton("Test Button");
testButton.addActionListener(new ActionListener(){
@Override public void actionPerformed(ActionEvent ae){
System.out.println("Click Detected by Anon Class");
}
});

testButton.addActionListener(e -> System.out.println("Click Detected by Lambda Listner"));

// Swing stuff
JFrame frame = new JFrame("Listener Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(testButton, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}

注意 Lambda 表达式作为参数被传入。类型自动匹配可以应用在很多场景中,以下举例说明:

  • Variable declarations(变量声明)
  • Assignments(赋值)
  • Return statements(返回语句)
  • Array initializers(数组初始化)
  • Method or constructor arguments(方法或构造器参数)
  • Lambda expression bodies(Lambda 表达式体)
  • Conditional expressions?:(三元运算符?:)
  • Cast expressions(类型转换语句)

简单说就是所有为expression的地方,都可以使用 Lambda 表达式,它本身就是一个 expression/statement。

资源下载

本小节中的 NetBeans 工程样例代码可以在下面下载到

LambdaExamples01.zip

通过 Lambda 表达式优化代码

在你掌握了前面展示的例子后,本节将介绍如何借助 Lambda 改进你的代码。Lambda 表达式将帮助你轻松实现“Don’t Repeat Yourself(DRY)”原则,并且使你的代码更加简洁易于阅读。

一个查询的 Case

一个常见的应用程序场景是,从一个集合中遍历,找出那些符合某种过滤条件的成员。在 2012 年的 JavaOne 大会上,Stuart Marks 和 Mike Duigou 通过格外出色的Jump-Starting Lambda展示了这种应用场景。给你一个名单列表以及一系列筛选条件,需要找出那些符合条件的人并且自动呼叫他们。这个指南在有一些轻微变更的情况下实现了上述需求。

在这个例子中,我们需要从以下三个美国群体中提取出消息:

  • 司机: 年龄大于 16 岁
  • 入伍人员: 男性,18 ~ 25 岁
  • 飞行员(特指商业飞行员): 23 ~ 65 岁

我们将要实现上述需求,不过不是通过打电话或者发邮件,而是在控制台输出信息。信息内容包括姓名、年龄和联系方式(例如邮件地址或者电话号码)

Person 类

属性定义如下:

1
2
3
4
5
6
7
8
9
10
public class Person {
private String givenName;
private String surName;
private int age;
private Gender gender;
private String eMail;
private String phone;
private String address;

}

Person类通过Builder来创建新对象,使用createShortList来创建人员列表。如下方代码片段所示。注意: 以下所有样例代码都可以在本节末尾给出的 NetBeans 工程里找到。

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
public static List<Person> createShortList(){
List<Person> people = new ArrayList<>();

people.add(
new Person.Builder()
.givenName("Bob")
.surName("Baker")
.age(21)
.gender(Gender.MALE)
.email("bob.baker@example.com")
.phoneNumber("201-121-4678")
.address("44 4th St, Smallville, KS 12333")
.build()
);

people.add(
new Person.Builder()
.givenName("Jane")
.surName("Doe")
.age(25)
.gender(Gender.FEMALE)
.email("jane.doe@example.com")
.phoneNumber("202-123-4678")
.address("33 3rd St, Smallville, KS 12333")
.build()
);

people.add(
new Person.Builder()
.givenName("John")
.surName("Doe")
.age(25)
.gender(Gender.MALE)
.email("john.doe@example.com")
.phoneNumber("202-123-4678")
.address("33 3rd St, Smallville, KS 12333")
.build()
);

第一次尝试

有了上面的Person类,加上定义好的检索条件,你可以写一个RoboContact类。一个可以参考的写法中定义了每种使用场景里的方法:

RoboContactMethods.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
package com.example.lambda;

import java.util.List;

/**
*
* @author MikeW
*/
public class RoboContactMethods {

public void callDrivers(List<Person> pl){
for(Person p:pl){
if (p.getAge() >= 16){
roboCall(p);
}
}
}

public void emailDraftees(List<Person> pl){
for(Person p:pl){
if (p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE){
roboEmail(p);
}
}
}

public void mailPilots(List<Person> pl){
for(Person p:pl){
if (p.getAge() >= 23 && p.getAge() <= 65){
roboMail(p);
}
}
}


public void roboCall(Person p){
System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
}

public void roboEmail(Person p){
System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
}

public void roboMail(Person p){
System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
}

}

如上所示,上面列出的方法说明了对不同对象采取的行为(打电话、发电子邮件、发实体邮件)。可以清楚地看出过滤条件和采取的行为。然而,这种设计存在一些缺陷:

  • 没有践行 DRY 原则
    • 每一个方法都是重复的模版
    • 必须为每个方法重写过滤条件
  • 每个用户场景都需要实现大量的方法(比如今天是打电话给入伍人员,发邮件给飞行员;明天可能就是打电话给飞行员,发邮件给入伍人员)
  • 代码呆板。如果过滤条件发生改变,不得不改动大量代码。因此,这段代码是难以维护的。

重构这些方法

如何修复这个类?可以从过滤条件入手。如果可以把过滤调价提取出一个独立的方法,将会有一些提升。

RoboContactMethods2.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
52
53
54
55
56
57
58
59
package com.example.lambda;

import java.util.List;

/**
*
* @author MikeW
*/
public class RoboContactMethods2 {

public void callDrivers(List<Person> pl){
for(Person p:pl){
if (isDriver(p)){
roboCall(p);
}
}
}

public void emailDraftees(List<Person> pl){
for(Person p:pl){
if (isDraftee(p)){
roboEmail(p);
}
}
}

public void mailPilots(List<Person> pl){
for(Person p:pl){
if (isPilot(p)){
roboMail(p);
}
}
}

public boolean isDriver(Person p){
return p.getAge() >= 16;
}

public boolean isDraftee(Person p){
return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
}

public boolean isPilot(Person p){
return p.getAge() >= 23 && p.getAge() <= 65;
}

public void roboCall(Person p){
System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
}

public void roboEmail(Person p){
System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
}

public void roboMail(Person p){
System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
}

}

比起上一个例子,这次的过滤条件被封装在单独的方法里。这些判断用的代码可以复用,需求变更时也避免了在类里面进行大范围的修改。然而,仍然存在着大量重复代码,以及必需为每种联系方法(打电话、发电子邮件、发邮件)单独写一个方法。是否有更好的将过滤条件传递给这些联系方法的方式?

匿名内部类

在 Lambda 表达式问世之前,可采用匿名内部类的方案。例如,一个解决方法是声明一个具有返回布尔值的test方法的MyTest.java类。把过滤条件在这个方法里,如下:

1
2
3
public interface MyTest<T> {
public boolean test(T t);
}

更新后的模拟联系方式类如下:

RobocontactAnon.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
public class RoboContactAnon {

public void phoneContacts(List<Person> pl, MyTest<Person> aTest){
for(Person p:pl){
if (aTest.test(p)){
roboCall(p);
}
}
}

public void emailContacts(List<Person> pl, MyTest<Person> aTest){
for(Person p:pl){
if (aTest.test(p)){
roboEmail(p);
}
}
}

public void mailContacts(List<Person> pl, MyTest<Person> aTest){
for(Person p:pl){
if (aTest.test(p)){
roboMail(p);
}
}
}

public void roboCall(Person p){
System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
}

public void roboEmail(Person p){
System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
}

public void roboMail(Person p){
System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
}

}

这里无疑是有了一些优化,因为只需要 3 个用于进行联系的方法。然而,在实际进行调用时,代码会有一些丑陋。看下面的测试代码:

RoboCallTest03.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
package com.example.lambda;

import java.util.List;

/**
* @author MikeW
*/
public class RoboCallTest03 {

public static void main(String[] args) {

List<Person> pl = Person.createShortList();
RoboContactAnon robo = new RoboContactAnon();

System.out.println("\n==== Test 03 ====");
System.out.println("\n=== Calling all Drivers ===");
robo.phoneContacts(pl,
new MyTest<Person>(){
@Override
public boolean test(Person p){
return p.getAge() >=16;
}
}
);

System.out.println("\n=== Emailing all Draftees ===");
robo.emailContacts(pl,
new MyTest<Person>(){
@Override
public boolean test(Person p){
return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
}
}
);


System.out.println("\n=== Mail all Pilots ===");
robo.mailContacts(pl,
new MyTest<Person>(){
@Override
public boolean test(Person p){
return p.getAge() >= 23 && p.getAge() <= 65;
}
}
);


}
}

这是“垂直”代码的一个典型案例。这样的代码难以阅读。此外,我们必须为每一种使用场景单独写自定义的过滤条件。

Lambda 表达式解千愁

Lambda 表达式可以解决目前为止我们遇到的一切问题。不过,让我们先了解一点别的。

java.uitl.function

在上一个例子中,MyTest函数接口将匿名内部类传递给方法调用。然而,写一个接口并不是必需的。Java SE 8 提供了java.util.function包,其中有一系列标准的函数借口。在这种场景下,Predicate接口符合我们的需求。

Predict意为“谓语,断言”,即判断条件的意思

1
2
3
public interface Predicate<T> {
public boolean test(T t);
}

test方法接收一个泛型类,返回一个布尔值。这正是我们所需要的过滤场景。下面是模拟联系方式类的最终版本。

RobocontactsLambda.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
package com.example.lambda;

import java.util.List;
import java.util.function.Predicate;

/**
*
* @author MikeW
*/
public class RoboContactLambda {
public void phoneContacts(List<Person> pl, Predicate<Person> pred){
for(Person p:pl){
if (pred.test(p)){
roboCall(p);
}
}
}

public void emailContacts(List<Person> pl, Predicate<Person> pred){
for(Person p:pl){
if (pred.test(p)){
roboEmail(p);
}
}
}

public void mailContacts(List<Person> pl, Predicate<Person> pred){
for(Person p:pl){
if (pred.test(p)){
roboMail(p);
}
}
}

public void roboCall(Person p){
System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
}

public void roboEmail(Person p){
System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
}

public void roboMail(Person p){
System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
}

}

在这种写法里,只需要三个方法用来模拟联系候选人。Lambda 表达式作为参数被传递给方法,用来过滤需要联系的人员。

垂直问题迎刃而解

Lambda 表达式解决了垂直代码的问题,并且更容易重用。来看看新的使用了 Lambda 表达式的测试类。

RoboCallTest04.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
package com.example.lambda;

import java.util.List;
import java.util.function.Predicate;

/**
*
* @author MikeW
*/
public class RoboCallTest04 {

public static void main(String[] args){

List<Person> pl = Person.createShortList();
RoboContactLambda robo = new RoboContactLambda();

// Predicates
Predicate<Person> allDrivers = p -> p.getAge() >= 16;
Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;

System.out.println("\n==== Test 04 ====");
System.out.println("\n=== Calling all Drivers ===");
robo.phoneContacts(pl, allDrivers);

System.out.println("\n=== Emailing all Draftees ===");
robo.emailContacts(pl, allDraftees);

System.out.println("\n=== Mail all Pilots ===");
robo.mailContacts(pl, allPilots);

// Mix and match becomes easy
System.out.println("\n=== Mail all Draftees ===");
robo.mailContacts(pl, allDraftees);

System.out.println("\n=== Call all Pilots ===");
robo.phoneContacts(pl, allPilots);

}
}

注意到,我们为每组联系人(allDrivers, allDraftees, allPilots)声明了一个断言(Predicate)。你可以将任何一个断言传给模拟联系的方法。代码简洁易于阅读,并且减少了重复代码。

资源下载

本小节中的 NetBeans 工程样例代码可以在下面下载到

RoboCallExample.zip

java.util.function

上节介绍的Predicate并不是 Java SE 8 所提供的唯一函数接口。开发者可以使用一系列的标准接口帮助开发。

像我这么吊的
像我这么吊的
  • Predicate:参数对象的某个属性
  • Consumer:对参数对象执行的某种操作
  • Function:把 T 转换为 U
  • Supplieer:提供一个 T 的实例(例如工厂)
  • UnaryOperator:一个单元运算符,T -> T
  • BinaryOperator:一个双元运算符,(T, T) -> T

此外,很多接口都有原始形式(类似上文中的Predicate和人工过滤器这种比较),可以帮助你更好地理解 Lambda 表达式。

东方取名法以及方法调用

在写上面一个例子时,我认为Person类应当具备一个灵活的输出系统。一个需求是以西方和东方两种命名方式打印出名字。在西方,名在前,姓在后。在东方则恰好相反。

老式写法的例子

这是在未使用 Lambda 表达式时的写法

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void printWesternName(){

System.out.println("\nName: " + this.getGivenName() + " " + this.getSurName() + "\n" +
"Age: " + this.getAge() + " " + "Gender: " + this.getGender() + "\n" +
"EMail: " + this.getEmail() + "\n" +
"Phone: " + this.getPhone() + "\n" +
"Address: " + this.getAddress());
}



public void printEasternName(){

System.out.println("\nName: " + this.getSurName() + " " + this.getGivenName() + "\n" +
"Age: " + this.getAge() + " " + "Gender: " + this.getGender() + "\n" +
"EMail: " + this.getEmail() + "\n" +
"Phone: " + this.getPhone() + "\n" +
"Address: " + this.getAddress());
}

你需要两个方法,分别打印西方命名、东方命名

Function接口

Function接口适用于这个问题。它只有一个名为apply的方法,方法签名如下:

1
public R apply(T t){ }

它接收一个泛型 T 的参数,然后返回一个 R 的对象。对于这个例子,传入Person类型,返回String类型。一个更加灵活的打印方法如下所示:

Person.java

1
2
3
public String printCuston(Function <Person, String> f) {
return f.apply(this);
}

这可真简单。方法接收一个Function参数,返回一个字符串。apply方法通过一个 Lambda 表达式返回Person对象的信息。

Function是如何定义的?如下是测试的样例代码。

NameTestNew.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
52
53
public class NameTestNew {

public static void main(String[] args) {

System.out.println("\n==== NameTestNew02 ===");

List<Person> list1 = Person.createShortList();

// Print Custom First Name and e-mail
System.out.println("===Custom List===");
for (Person person:list1){
System.out.println(
person.printCustom(p -> "Name: " + p.getGivenName() + " EMail: " + p.getEmail())
);
}


// Define Western and Eastern Lambdas

Function<Person, String> westernStyle = p -> {
return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" +
"Age: " + p.getAge() + " " + "Gender: " + p.getGender() + "\n" +
"EMail: " + p.getEmail() + "\n" +
"Phone: " + p.getPhone() + "\n" +
"Address: " + p.getAddress();
};

Function<Person, String> easternStyle = p -> "\nName: " + p.getSurName() + " "
+ p.getGivenName() + "\n" + "Age: " + p.getAge() + " " +
"Gender: " + p.getGender() + "\n" +
"EMail: " + p.getEmail() + "\n" +
"Phone: " + p.getPhone() + "\n" +
"Address: " + p.getAddress();

// Print Western List
System.out.println("\n===Western List===");
for (Person person:list1){
System.out.println(
person.printCustom(westernStyle)
);
}

// Print Eastern List
System.out.println("\n===Eastern List===");
for (Person person:list1){
System.out.println(
person.printCustom(easternStyle)
);
}


}
}

第一个循环只是打印出名字和电子邮件地址。不过可以把任何的表达式传给printCuston方法。东方命名法和西方命名法使用 Lambda 表达式定义,并且保存在变量中。这些变量接下来被传入最后的两个循环。可以很容易地将 Lambda
表达式合并成为Map,用起来更加方便。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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
==== NameTestNew02 ===
===Custom List===
Name: Bob EMail: bob.baker@example.com
Name: Jane EMail: jane.doe@example.com
Name: John EMail: john.doe@example.com
Name: James EMail: james.johnson@example.com
Name: Joe EMail: joebob.bailey@example.com
Name: Phil EMail: phil.smith@examp;e.com
Name: Betty EMail: betty.jones@example.com

===Western List===

Name: Bob Baker
Age: 21 Gender: MALE
EMail: bob.baker@example.com
Phone: 201-121-4678
Address: 44 4th St, Smallville, KS 12333

Name: Jane Doe
Age: 25 Gender: FEMALE
EMail: jane.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

Name: John Doe
Age: 25 Gender: MALE
EMail: john.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

Name: James Johnson
Age: 45 Gender: MALE
EMail: james.johnson@example.com
Phone: 333-456-1233
Address: 201 2nd St, New York, NY 12111

Name: Joe Bailey
Age: 67 Gender: MALE
EMail: joebob.bailey@example.com
Phone: 112-111-1111
Address: 111 1st St, Town, CA 11111

Name: Phil Smith
Age: 55 Gender: MALE
EMail: phil.smith@examp;e.com
Phone: 222-33-1234
Address: 22 2nd St, New Park, CO 222333

Name: Betty Jones
Age: 85 Gender: FEMALE
EMail: betty.jones@example.com
Phone: 211-33-1234
Address: 22 4th St, New Park, CO 222333

===Eastern List===

Name: Baker Bob
Age: 21 Gender: MALE
EMail: bob.baker@example.com
Phone: 201-121-4678
Address: 44 4th St, Smallville, KS 12333

Name: Doe Jane
Age: 25 Gender: FEMALE
EMail: jane.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

Name: Doe John
Age: 25 Gender: MALE
EMail: john.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

Name: Johnson James
Age: 45 Gender: MALE
EMail: james.johnson@example.com
Phone: 333-456-1233
Address: 201 2nd St, New York, NY 12111

Name: Bailey Joe
Age: 67 Gender: MALE
EMail: joebob.bailey@example.com
Phone: 112-111-1111
Address: 111 1st St, Town, CA 11111

Name: Smith Phil
Age: 55 Gender: MALE
EMail: phil.smith@examp;e.com
Phone: 222-33-1234
Address: 22 2nd St, New Park, CO 222333

Name: Jones Betty
Age: 85 Gender: FEMALE
EMail: betty.jones@example.com
Phone: 211-33-1234
Address: 22 4th St, New Park, CO 222333

资源下载

本小节中的 NetBeans 工程样例代码可以在下面下载到

LambdaFunctionExamples.zip

Lambda 表达式和集合

上节介绍了Function接口和示例语法。本节我们将看到 Lambda 表达式如何提升Collections类。

Lambda 表达式和集合

到目前为止的例子中,集合类出现的很少。然而,相当一部分 Lambda 表达式的特征改变了我们使用集合类的方式。本节介绍其中的一部分。

类增强

司机、飞行员、入伍人员的过滤条件被包装进了SearchCriteria类。

SearchCriteria.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
package com.example.lambda;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Predicate;

/**
*
* @author MikeW
*/
public class SearchCriteria {

private final Map<String, Predicate<Person>> searchMap = new HashMap<>();

private SearchCriteria() {
super();
initSearchMap();
}

private void initSearchMap() {
Predicate<Person> allDrivers = p -> p.getAge() >= 16;
Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;

searchMap.put("allDrivers", allDrivers);
searchMap.put("allDraftees", allDraftees);
searchMap.put("allPilots", allPilots);

}

public Predicate<Person> getCriteria(String PredicateName) {
Predicate<Person> target;

target = searchMap.get(PredicateName);

if (target == null) {

System.out.println("Search Criteria not found... ");
System.exit(1);

}

return target;

}

public static SearchCriteria getInstance() {
return new SearchCriteria();
}
}

本例的编码不是很规范,作为参数的PredicateName应该写为predicateName更严谨。

这个类保存了基于Predicate的搜索过滤条件,你可以在测试用例中使用它们。

循环

首先登场的是任何集合类都可以使用的forEach方法。这里是一些打印出Person列表的例子。

Test01ForEach.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test01ForEach {

public static void main(String[] args) {

List<Person> pl = Person.createShortList();

System.out.println("\n=== Western Phone List ===");
pl.forEach( p -> p.printWesternName() );

System.out.println("\n=== Eastern Phone List ===");
pl.forEach(Person::printEasternName);

System.out.println("\n=== Custom Phone List ===");
pl.forEach(p -> { System.out.println(p.printCustom(r -> "Name: " + r.getGivenName() + " EMail: " + r.getEmail())); });

}

}

例 1 展示了一个 Lambda 表达式,它调用printWesternName方法打印出列表里每个人的名字。例 2 展示了一个 方法调用。当已经存在对类对象操作的方法时,这种写法可以代替通用 Lambda 表达式的写法。最后,例 3 展示了printCustom方法也可以在这种场景下使用。注意 Lambda 表达式嵌套时,变量名的细小差别。

你可以用这种方式遍历任何集合。基础结构与增强型for训话类似。然而,一些增强型机制能带来很多好处。

链式调用与过滤器

除了遍历集合内元素,你还可以将方法组装成链式。第一个要讲解的方法是filter,它接收一个Predicate类型的参数。

下面例子在过滤元素后,遍历列表。

Test02Filter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test02Filter {

public static void main(String[] args) {

List<Person> pl = Person.createShortList();

SearchCriteria search = SearchCriteria.getInstance();

System.out.println("\n=== Western Pilot Phone List ===");

pl.stream().filter(search.getCriteria("allPilots"))
.forEach(Person::printWesternName);


System.out.println("\n=== Eastern Draftee Phone List ===");

pl.stream().filter(search.getCriteria("allDraftees"))
.forEach(Person::printEasternName);

}
}

第一个和最后一个循环展示了List如何基于过滤条件被过滤。控制台输出如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
=== Eastern Draftee Phone List ===

Name: Baker Bob
Age: 21 Gender: MALE
EMail: bob.baker@example.com
Phone: 201-121-4678
Address: 44 4th St, Smallville, KS 12333

Name: Doe John
Age: 25 Gender: MALE
EMail: john.doe@example.com
Phone: 202-123-4678
Address: 33 3rd St, Smallville, KS 12333

偷点儿懒

这些特征虽然有价值,但是在已经有了一个完美的for循环语法的情况下,把它们引入集合类的意义何在?通过将集合操作归纳成库,Java 开发者可以在代码上进行更多的优化。首先看一对名词解释。

  • 懒惰:在编程领域,懒惰意味着只处理你需要处理的对象。在先前的例子中,最后一个循环是“懒惰”的,因为它只遍历了List被过滤出来的两个Person对象。这样的代码效率更高,因为最后只处理了两个对象,(而非整个列表)。
  • 主动性:处理列表中每个元素的代码被称为“主动性强”的。例如,一个增强型for循环遍历整个列表,只为处理 2 个元素,这种方式被认为“主动性”很强。

stream方法

在先前的代码样例中,你会发现在对列表进行过滤和遍历之前,我们调用了stream方法。这个方法接收Collection类型的参数,返回java.util.stream.Stream类型的结果。一个Stream对象意味着一个元素序列,你可以在上面进行链式操作。默认地,一旦元素被消费了,他们就不再处于 Stream 中。此外,随着调用方法不同,Stream可以用在串行(默认)或者并行的场景中。本节末尾将介绍一个并行的场景。

变更与运算结果

如刚才所提到的,Stream在使用完成后就会被丢弃。因此,我们无法通过Stream来对列表中的元素进行修改。然而,如果你想要保存链式处理后的元素呢?你可以将它们存入一个新的集合。如下代码所示:

Test03toList.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test03toList {

public static void main(String[] args) {

List<Person> pl = Person.createShortList();

SearchCriteria search = SearchCriteria.getInstance();

// Make a new list after filtering.
List<Person> pilotList = pl
.stream()
.filter(search.getCriteria("allPilots"))
.collect(Collectors.toList());

System.out.println("\n=== Western Pilot Phone List ===");
pilotList.forEach(Person::printWesternName);

}

}

collect方法只有一个参数,是Collector类。Collector类用于从处理流的结果中生成一个List或者Set。上例介绍了如何将列表过滤后的结果存成List

使用map进行计算

map方法通常和filter搭配使用。这个方法去除类里面的某个属性,然后对它进行操作。如下代码基于年龄字段进行了一系列计算。

Test04Map.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
public class Test04Map {

public static void main(String[] args) {
List<Person> pl = Person.createShortList();

SearchCriteria search = SearchCriteria.getInstance();

// Calc average age of pilots old style
System.out.println("== Calc Old Style ==");
int sum = 0;
int count = 0;

for (Person p:pl){
if (p.getAge() >= 23 && p.getAge() <= 65 ){
sum = sum + p.getAge();
count++;
}
}

long average = sum / count;
System.out.println("Total Ages: " + sum);
System.out.println("Average Age: " + average);


// Get sum of ages
System.out.println("\n== Calc New Style ==");
long totalAge = pl
.stream()
.filter(search.getCriteria("allPilots"))
.mapToInt(p -> p.getAge())
.sum();

// Get average of ages
OptionalDouble averageAge = pl
.parallelStream()
.filter(search.getCriteria("allPilots"))
.mapToDouble(p -> p.getAge())
.average();

System.out.println("Total Ages: " + totalAge);
System.out.println("Average Age: " + averageAge.getAsDouble());

}

}

输出如下:

1
2
3
4
5
6
7
== Calc Old Style ==
Total Ages: 150
Average Age: 37

== Calc New Style ==
Total Ages: 150
Average Age: 37.5

这段代码计算列表中所有飞行员的平均年龄。第一个循环展示了老式用for循环的写法。第二个循环使用了map方法获取串流中的每个人的年龄。注意totalAge是一个long类型对象。map方法返回一个IntStream对象,可以对其调用sum方法,这会返回一个long值。

注意: 第二次计算平均年龄时,再对所有值求和是多余的。然而,上例中这样写是为了展示sum函数的调用方法。

最后一个循环基于串流计算平均年龄。注意parallelStream方法,它是 Java 8 中流式计算的并行版本,返回结果也不是简单的double,而是OptionalDouble

parallelStream提供了一种并发处理集合元素的方法,以上文的列表求平均值为例,如果一共有 100 个元素,parallelStream会将任务分为 n 份(n 是线程池的大小,默认为 CPU 核数),每份任务交由单独一个线程处理,最终再汇总这些任务的结果,得出平均值

资源下载

本小节中的NetBeans工程样例代码可以在下面下载到

LambdaCollectionExamples.zip

总结

在本篇指南中,你学会了使用以下技能:

  • Java 匿名内部类
  • 使用 Java SE 8 的 Lambda 表达式代替匿名内部类
  • Lambda 表达式的语法
  • 通过Predicate接口在列表中查找
  • 通过Function接口处理对象,并得到一个新类型的对象
  • Java SE 8 中新增的支持 Lambda 表达式的集合特性

更多资源

如果你想要了解更多关于 Lambda 表达式和 Java SE 8 的信息,参考以下链接:

著者

  • 课程组长:Michael Williams
  • QA:Juan Quesada Nunez
  • 翻译:李磊