深入 Java Lambda 一:为什么需要 Lambda

前言

本文是由笔者所原创的深入 Java Lambda 系列之一,本文主要从根源上描述为什么需要 Lambda?

本文为作者原创作品,转载请注明出处;

概述

本文主要通过一系列的例证,描述为什么 Java 在某些方面显得很笨重,为什么会产生所谓的 Vertical Problem 既是纵向问题,而又如何通过 Java Lambda 来解决这样的问题的;

为什么需要 Lambda

Vertical Problem

Vertical Problem,既是纵向问题;

我们把时间回到 2012 年 JavaOne 大会上,在 “Jump-Starting Lambda” 上 Stuart Marks 和 Mike Duigou 通过一系列的例子描述了 Java 目前所所存在的一系列的问题,并通过现有的方法逐步给出解决办法并最终推导出所谓的Vertical Problem;下面笔者将相关的推导过程和最终导出Vertical Problem的过程整理如下,

首先,我们有这样的一个需求,需要根据如下三个不同分组采用不同的方式给他们分别发送消息,

  • Drivers: 年龄大于 16 岁的公民(可以申请司机);消息发送的方式,打电话;
  • Draftees: 18 到 25 岁的男性;消息发送的方式,电邮 email;
  • Pilots: 23 到 65 岁的公民;消息发送的方式,纸质邮件的方式 mail;

我们通过 Person 来描述上述的公民信息,

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

private String givenName;

private String surName;

private int age;

private Gender gender;

private String eMail;

private String phone;

private String address;

...
}

然后,构建相关的测试用例;

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 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()
);
}

第一次尝试为不同的分组发送不同的消息,

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
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 原则( Don’t Repeat Yourself )

    1. 每一个方法都重复类似的循环机制;
    2. 必须为每个方法构建不同的选择条件逻辑 Search Criteria;
  • 如果将来有更多的类型,则需要大量的类似的代码需要实现;

  • 代码不够灵活,总结起来有如下两个弊端,

    1. Search Criteria 是写死在方法逻辑中的,不能重用;
    2. Search Criteria 是耦合到模块方法中的,如果将来 Search Criteria 需要发生变化,那么模块中的方法也需要修改;不利于将来代码的进行模块化设计和维护;(这里所要达到的目标是,RoboContactMethods 能够通用并能够单独成为一个模块,而不用根据 Search Criteria 的变化而进行修改,这样 RoboContactMethods 就可以被设计为一个单独的模块了;)

第一次尝试重构,将 Search Criteria 进行重构,将 Search Criteria 封装到其它方法中便于重用,

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
   
import java.util.List;

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());
}

}

非常简单直接,我们将 Search Criteria 封装到各自不同的用于条件判断的方法中,isDriver(Person p)isDraftee(Person p)isPilot(Person p);好处是,Search Criteria 通过方法的包装达到了其被重用的目的;但是,问题是,核心模块方法 callDriversemailDraftees 以及 mailPilot 中依然强依赖上述的判断条件方法,如果将来要做模块拆分,那么势必将核心模块方法所有的判断条件方法包含成同一个模块,但是,如果我们要将判断条件放开,交由第三方模块自定义呢?目前唯有的解决方法就是需要为判断条件方法做统一的接口,这样就可以将判断条件交由第三方模块去任意定制实现了,即可做到了核心模块和条件判断模块的的分离;

第二次尝试重构,按照笔者上述重构的思路,可以通过构建一个统一的接口来达到这个目的;

创建一个统一的接口 MyTest,

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

至此,我们将 RoboContactMethods2.java 继续重构,得到的代码如下,

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());
}

}

这样,判断条件需要由调用方模块提供,

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
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;
}
}
);

}
}

可以看到,我们成功的将 Search Criteria 通过统一接口的方式交由第三方模块来定制了,它不再与核心模块相耦合了,这样有利于核心模块和 Search Criteria 相关的非和核心模块进行模块化的设计和拆分;但是,匿名类的方式在业界通常为人诟病,那就是,我只想实现一行代码的逻辑,往往需要写上 4、5 行的代码,而且大部分代码与我核心的那一行代码并无直接关系,这就是所谓的Vertical Problem,既是代码的纵向问题;比如,

1
2
3
4
5
6
7
8
robo.emailContacts(pl, 
new MyTest<Person>(){
@Override
public boolean test(Person p){
return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
}
}
);

那么有没有这样一种简洁的表达方式,来解决上述的Vertical Problem呢?答案就是使用 Lambda;

使用 Lambda 解决 Vertical Problem

从上述的小节,我们知道,通过构造接口的方式可以实现核心模块与 Search Criteria 非核心模块代码逻辑之间的解耦;但是,却引入了Vertical Problem;所以,本小节开始,笔者将简要介绍,通过 Lambda 是如何解决Vertical Problem的;这里,笔者并不会重点介绍什么是 Lambda,有关 Lambda 的相关概念将在后续的文章中进行详细的介绍,这里,我们只来简要的看一下,通过 Lambda,我们是如何解决Vertical Problem的;

首先,我们利用 Java 8 java.util.function 包中所提供的现成的接口 Predicate

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

继续将代码重构如下,

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
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 来构建 Search Criteria,

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

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);

}
}

可以看到,之前的匿名接口类所实现的逻辑,被一行代码所取代了,

1
2
3
4
// 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;

因此,通过 Lambda 的方式,解决了上述的 Vertical Problem,这里唯一需要知道的是,代码,

1
Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;

等价于如下匿名接口类的实现,得到的就是一个实现了 Predicate 接口的匿名类实例;

1
2
3
4
5
6
new MyTest<Person>(){
@Override
public boolean test(Person p){
return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
}
}

Reference

本文的思路主要来源于:http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/Lambda-QuickStart/index.html

http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html

java 8 interface default method: http://blog.csdn.net/wwwsssaaaddd/article/details/24213525