Java8新特性一:Lambda Expressions | Java提升营

Java8新特性一:Lambda Expressions

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表达式就是这种类型。

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

给老奴加个鸡腿吧 🍨.