使用lambda表达式改进代码
本节基于前面的示例来展示lambda表达式如何改进代码. lambda应该提供一种方法来更好地支持不重复自己(DRY)原则, 使你的代码更简单,更可读。
常见查询用例
一个常见的编程用例是搜索数据集合以查找符合特定条件的项目. 在JavaOne 2012的出色的"Jump-Starting Lambda"演示中, Stuart Marks和Mike Duigou演示过这样的用例. 给定人的列表, 使用各种标准来向匹配的人做出robo呼叫(自动电话呼叫). 本教程遵循稍微变化的基本前提.
在这个例子中, 我们的消息需要到美国的三个不同的组: * 司机: 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方法来创建新对象. 一个示例people列表由createShortList方法创建:
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类. 一种可能的解决方案是为每个用例定义一个方法:
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原则
- 每种方法重复循环机制
- 必须为每个方法重写选择条件
- 需要大量的方法来实现每个用例
- 代码不灵活. 如果搜索条件更改, 则需要对更新进行大量代码更改. 因此, 代码可维护性不好
重构方法
如何修正这个类? 搜索条件是一个很好的起点. 如果测试条件被分离到单独的方法中, 那将是一种改进.
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表达式之前, 匿名内部类曾是一种选择. 比如, 一个函数式接口就是一个可行的解决方案. 当这个接口被调用时, 搜索条件就可以传入. 接口看起来像这样:
public interface MyTest<T>{
public boolean test(T t);
}
而更新了的自动化类如下:
package com.example.lambda;
import java.util.List;
/**
*
* @author MikeW
*/
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());
}
}
这无疑是另一个进步, 因为只需要三个方法就能实现自动化操作. 然而, 当调用方法时还有一个丑陋的小问题. 查看用于此类的测试类:
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方法接收一个泛型类并返回布尔值. 这正适合用来做选择. 最终的代码如下:
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表达式更新的新测试类.
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接口中的任何一个传递给联系方法. 代码紧凑, 易于阅读, 不拖沓.