使用Lambda表达式改进代码
本章节根据前面的例子来向你展示Lambda表达式是如何改进你的代码的.Lambda表达式更好地支持不要重复自己(Donot Repeat Yourself)原则,并使你的代码更简单和更具有可读性。
一个常见的查询用例
一个常见的编程用例就是根据特定的规则从数据集合中查找元素。在2012年JavaOne大会上精彩的“Jump-Starting Lambda”演说中,Stuart Marks 和 MikeDuigou就是使用这个用例做示范。给一个人名单,使用不同规则让机器人和匹配的人通话。这个教程也遵循这一基本前提但稍微有些变化。
在这个示例中,我们的信息需要区分在美国的三个不同的群体。
- 司机:年龄在16岁以上的
- 应征者:年龄在18-25岁的男性
- 飞行员(特指商业飞行员):年龄在23-65之间
能够完成这些任务的真实机器人还没有进入商用阶段.这里不打电话,邮寄或者发送电子邮件,取而代之是将信息在控制台打印出来.信息包含一个人的名字,年龄和一些特定的媒体信息(例如用来发送电邮的电子邮箱地址,打电话所需要的电话号码)
Person Class
在测试名单中的人都是使用Person类来定义并具有以下的属性:
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
工程中,在这一章节的最后有链接地址。
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()
);
第一次尝试
Pserson类和查询条件都已经定义好,现在你可以写一个RoboContant类。一个可行的解决方法是给每一个用例定义一个方法:
RoboContactsMethods.java
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());
}
}
你可以从方法名(callDrivers, emailDraftees, 和 mailPilots)中明白这些方法描述的都是一种正在发生的行为。只要传达明确的查询条件,机器人根据适当的调用做出动作。可是,这样的设计也有几个不好的地方:
- 没有遵循“不要重复自己”(DRY)原则
每个方法都重复循环机制
每个方法的选择条件必须重写
- 针对每个用例好多方法需要实现
- 代码不灵活。一旦查询条件变化,许多的代码都需要为一个更新而改变。此外,这个代码也不好维护。
重构方法
怎么修改代码呢?从查询条件开始比较好。如果测试条件能够独立放在一个方法里,将会好很多。
RoboContactMethods2.java
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表达式之前,匿名内部类是一种选择。例如给一个接口(MyTest.java)定义一个Test方法返回布尔值(函数式接口)是一个可行的解决方法。查询条件当这个方法被调用的时候被传递过去。这个接口就像下面的那样:
public interface MyTest<T> {
public boolean test(T t);
}
更新之后的robot类就像这样的:
RoboContactsAnon.java
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());
}
}
这无疑是一种改进的方法,因为只需要三个方法来执行机械手操作。可是,有个小问题就是方法在被调用的时候不优雅。我们看下这个类的测试类:
RoboCallTest03.java
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.util.function
在上一个例子中,MyTest函数式接口将匿名内部类传给方法.可是,不必要再单独写个接口.Java SE 8提供了java.util.function包里面有许多标准的函数式接口.这个用例中,Predicate接口符合我们的需求。
public interface Predicate<T> {
public boolean test(T t);
}
test方法带有一个泛型类和返回一个boolean值。这正好是条件选择所需要的。下面是robot类的最后版本。
RoboContactsLambda.java
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表达式传递给方法来选择符合测试条件的Person实体。
“垂直问题”解决
Lambda表达式解决了垂直问题,任何表达式都能够很容易的复用。再来看一下经过Lambda表达式更改之后新的测试类。
RoboCallTest04.java
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);
}
}
注意每一组都设置了 Predicate : allDrivers , allDraftees , 和 allPilots 。 你可以传任意一个 Predicate 接口给这些联系方法。代码紧凑很容易读而且没有重复。
本章源码