Java8新特性一:Lambda Expressions

文章目录
  1. 1. Lambda表达式
  2. 2. Lambda表达式的用例
    1. 2.1. 搜索匹配一个特定特征的用户
    2. 2.2. 更通用的搜索方法
    3. 2.3. 在类中指定搜索条件
    4. 2.4. 在匿名类中指定搜索条件
    5. 2.5. 使用Lambda表达式指定搜索条件
    6. 2.6. 将functional interface与Lambda表达式一起使用
    7. 2.7. 更广泛的使用Lambda表达式
  3. 3. Lambda表达式的语法
  4. 4. 访问局部变量
  5. 5. 目标类型

Lambda表达式

匿名类存在的问题是: 如果匿名类的实现非常简单,例如仅包含一个方法的接口,则匿名类的语法可能看起来很笨拙且不清楚。在这些情况下,您通常 new一个匿名内部类对象作为参数传递给方法,例如,当某人单击按钮时应采取什么措施。Lambda表达式 能实现这样的需求,它可以更紧凑更简洁的表达单方法类的实例。

本篇文章从以下几点介绍一下Lambda表达式:

  1. Lambda表达式用例
    • 搜索匹配一个特征的用户
    • 更通用的搜索方法
    • 在类中指定搜索条件
    • 在匿名类中指定搜索条件
    • 使用Lambda表达式指定搜索条件
    • 将functional interface与Lambda表达式一起使用
    • 更广泛的使用Lambda表达式
  2. Lambda表达式的语法
  3. 访问局部变量
  4. 目标类型

Lambda表达式的用例

假设您正在开发一个社交网络程序。您想新增一个功能,使管理员可以对满足特定条件的社交网络用户执行任何类型的操作,例如发送消息。

用户用以下Person类表示:

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

public enum Sex {
MALE, FEMALE
}

String name;
LocalDate birthday;
Sex gender;
String emailAddress;

public int getAge() {
// ...
}

public void printPerson() {
// ...
}
}

并且用户存储在一个List<Person>实例中。

我们先从最笨的实现开始,然后使用本地和匿名类对该方法进行改进,最后再使用lambda表达式以一种高效而简洁的方式实现。

搜索匹配一个特定特征的用户

一种简单的实现是:创建几个方法,每个方法都会搜索和一个特征(例如性别或年龄)相匹配的成员。以下方法将打印出超过指定年龄的成员:

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

这样做可以满足业务需求,但是他的可扩展性非常差,并且每个特征的一种搜索需要写一个方法,很麻烦。可以考虑以下几个问题:

  1. 类的属性有100个甚至1000个呢?
  2. 如果要打印小于的顶年龄的成员呢?

更通用的搜索方法

以下方法比前面的 printPersonsOlderThan方法更通用,它会打印指定年龄范围内的成员:

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

考虑以下几个问题:

  1. 如果要打印指定性别或指定性别和年龄范围的组合,该怎么办?
  2. 如果您决定更改Person班级并添加其他属性,例如关系状态或地理位置,该怎么办?
  3. 尽管此方法比上面的printPersonsOlderThan通用,但尝试为每个特征创建单独的方法仍会导致代码脆弱。

在类中指定搜索条件

下面的方法打印和指定的搜索条件匹配的用户:

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

这个方法会遍历List中的Person对象,通过CheckPerson检查每个Person,如果满足搜索条件,就会输出Person信息。

要指定搜索条件,实现以下 CheckPerson接口:

1
2
3
interface CheckPerson {
boolean test(Person p);
}

下面的类实现了CheckPerson接口并且实现了接口中的test的方法,这个方法筛选男性且年龄在18至25岁之间的用户。

1
2
3
4
5
6
7
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}

使用CheckPerson

1
2
printPersons(
roster, new CheckPersonEligibleForSelectiveService());

在匿名类中指定搜索条件

以下方法传入的第二个参数是一个匿名类,该类筛选男性且年龄在18至25岁之间的用户:

1
2
3
4
5
6
7
8
9
10
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);

这种方法减少了代码量,因为您不必为每个搜索条件创建一个类。但是,考虑到CheckPerson接口仅包含一个方法,并且匿名类的代码相当庞大,在这种情况下,您可以使用lambda表达式代替匿名类。

使用Lambda表达式指定搜索条件

CheckPerson 接口是一个functional interfacefunctional interface是仅包含一个抽象方法的接口 。(functional interface可能包含一个或多个 默认方法静态方法。)由于functional interface仅包含一个抽象方法,因此在实现该方法时可以省略该方法的名称。因此,可以使用lambda表达式(而不是使用匿名类表达式)。

1
2
3
4
5
6
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);

可以使用functional interface来代替CheckPerson,这可以进一步减少所需的代码量。

将functional interface与Lambda表达式一起使用

1
2
3
interface CheckPerson {
boolean test(Person p);
}

CheckPerson一个非常简单的接口,是一个functional interface。因为JDK已经提供了一些通用的functional interface,可以在java.util.function包中找到它们。所以,我们可以直接使用这些functional interface,如果能够满足我们的需求,我们没必要再定义这样的接口。

例如,可以使用 Predicate<T> 接口代替CheckPerson。该接口包含方法boolean test(T t)

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

此接口仅包含一个参数类型T。当使用实际参数声明或实例化泛型类型时,您将拥有一个参数化类型。例如,参数化类型Predicate如下:

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

此参数化类型包含一个方法,该方法的参数和返回类型与CheckPerson.boolean test(Person p)相同。因此,可以用Predicate<T>代替CheckPerson

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

因此,以下方法调用和在类中指定搜索条件有相同的效果:

1
2
3
4
5
6
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);

这不是使用lambda表达式的唯一方式。

更广泛的使用Lambda表达式

重新看一下printPersonsWithPredicate方法,看看在什么地方还可以使用lambda表达式:

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

我们接下来将printPerson方法用Lambda表达式代替,那么我们需要一个functional interface,该方法可以传入一个Person类型的参数并返回void。 很幸运,JDK提供的 Consumer<T>接口满足这样的需求。

1
2
3
4
5
6
7
8
9
10
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}

调用该方法时的写法如下:

1
2
3
4
5
6
7
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);

如果想对个人资料进行更多处理而不仅仅打印出来,该怎么办?假设要验证用户的个人资料或检索他们的联系信息?在这种情况下,需要一个functional interface,其中包含一个有返回值的抽象方法。很幸运,JDK提供的 Function<T,R> 接口接口满足这样的需求。

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}
}

以下调用先从符合条件的Person中获取电子邮件信息,然后打印出来:

1
2
3
4
5
6
7
8
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);

Lambda表达式的语法

Lambda表达式包含以下内容:

  1. 参数以逗号分隔,用括号括起来。CheckPerson.test方法包含一个参数p, 它代表Person类的一个实例。

注意:可以省略lambda表达式中参数的数据类型。此外,如果只有一个参数,则可以省略括号。例如,以下lambda表达式也有效:

1
2
3
p-> p.getGender()== Person.Sex.MALE 
&& p.getAge()> = 18
&& p.getAge()<= 25
  1. 箭头标记 ->

  2. 由单个表达式或语句块组成。本示例使用以下表达式:

1
2
3
p.getGender()== Person.Sex.MALE 
&& p.getAge()> = 18
&& p.getAge()<= 25

如果指定单个表达式,将计算表达式并返回其值。另外,可以使用return语句:

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

return语句不是表达式。在lambda表达式中,必须将语句括在大括号{}中。但是,对void方法的调用不用括在大括号中。例如,以下是有效的lambda表达式:

1
email -> System.out.println(email)

注意,lambda表达式看起来很像方法声明。可以将lambda表达式视为匿名方法,即没有名称的方法。

以下示例, Calculator定义多个参数lambda表达式示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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表达式:additionsubtraction。示例输出以下内容:

1
2
40 + 2 = 42
20-10 = 10

访问局部变量

像本地和匿名类一样,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
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);
}
}

本示例输出:

1
2
3
4
x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0

如果在myConsumer声明里用参数x代替y,编译器将报错:

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

错误信息:methodInFirstLevel(int)方法已经定义了变量x 。这是因为lambda表达式未引入新的作用域级别。因此,您可以直接访问该范围的字段、方法和局部变量。例如,lambda表达式直接访问methodInFirstLevel方法的x参数。要访问类中的变量,请使用关键字this。在此示例中,this.x引用成员变量FirstLevel.x。

与本地和匿名类一样,lambda表达式只能访问用final或effectively final的局部变量和参数。例如,假设您在methodInFirstLevel方法内部添加以下赋值语句:

1
2
3
4
void methodInFirstLevel(int x){
x = 99;
// ...
}

由于改变了x的值,所以该变量不再是final或实际上final类型的变量。由于lambda表达式myConsumer会访问FirstLevel.x变量,结果Java编译器报一条错误信息,类似于lambda表达式引用的本地变量必须是final或实际上是final

1
System.out.println(“ x =” + x);

目标类型

您如何确定Lambda表达式的类型?回忆一下选择年龄在18至25岁之间的男性用户的lambda表达式:

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

此lambda表达式用在以下两个方法:

1
2
3
public static void printPersons(List<Person> roster, CheckPerson tester)

public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)

当调用printPersons方法时,它期望的数据类型为CheckPerson,因此lambda表达式为该类型。但是,当调用printPersonsWithPredicate方法时,它期望的数据类型为Predicate<Person>,因此lambda表达式就是这种类型。

这些方法期望的数据类型称为目标类型。