本文是由笔者所原创的 深入 Java Lambda 系列 之一,本文主要从根源上描述为什么需要 Lambda?
本文为作者原创作品,转载请注明出处;
本文主要通过一系列的例证,描述为什么 Java 在某些方面显得很笨重,为什么会产生所谓的 Vertical Problem 既是纵向问题,而又如何通过 Java Lambda 来解决这样的问题的;
Vertical Problem,既是纵向问题;
我们把时间回到 2012 年 JavaOne 大会上,在 “Jump-Starting Lambda”
上 Stuart Marks 和 Mike Duigou 通过一系列的例子描述了 Java 目前所所存在的一系列的问题,并通过现有的方法逐步给出解决办法并最终推导出所谓的 Vertical Problem
;下面笔者将相关的推导过程和最终导出 Vertical Problem
的过程整理如下,
首先,我们有这样的一个需求,需要根据如下三个不同分组采用不同的方式给他们分别发送消息,
我们通过 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; ... }
然后,构建相关的测试用例;
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
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 )
如果将来有更多的类型,则需要大量的类似的代码需要实现;
代码不够灵活,总结起来有如下两个弊端,
第一次尝试重构,将 Search Criteria 进行重构,将 Search Criteria 封装到其它方法中便于重用,
RoboContactMethods2.java
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 通过方法的包装达到了其被重用的目的;但是,问题是, 核心模块方法 callDrivers 、 emailDraftees 以及 mailPilot 中依然强依赖上述的判断条件方法,如果将来要做模块拆分,那么势必将 核心模块方法 所有的 判断条件方法 包含成同一个模块,但是,如果我们要将判断条件放开,交由第三方模块自定义呢?目前唯有的解决方法就是需要为 判断条件方法 做统一的接口,这样就可以将判断条件交由第三方模块去任意定制实现了,即可做到了核心模块和条件判断模块的的分离;
第二次尝试重构,按照笔者上述重构的思路,可以通过构建一个统一的接口来达到这个目的;
创建一个统一的接口 MyTest,
public interface MyTest<T> { public boolean test(T t); }
至此,我们将 RoboContactMethods2.java 继续重构,得到的代码如下,
RoboContactAnon.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()); } }
这样,判断条件需要由调用方模块提供,
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
,既是代码的 纵向问题
;比如,
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;
从上述的小节,我们知道,通过构造接口的方式可以实现核心模块与 Search Criteria 非核心模块代码逻辑之间的解耦;但是,却引入了 Vertical Problem
;所以,本小节开始,笔者将简要介绍,通过 Lambda 是如何解决 Vertical Problem
的;这里,笔者并不会重点介绍什么是 Lambda,有关 Lambda 的相关概念将在后续的文章中进行详细的介绍,这里,我们只来简要的看一下,通过 Lambda,我们是如何解决 Vertical Problem
的;
首先,我们利用 Java 8 java.util.function 包中所提供的现成的接口Predicate
public interface Predicate<T> { public boolean test(T t); }
继续将代码重构如下,
RoboContactsLambda.java
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
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); } }
可以看到,之前的匿名接口类所实现的逻辑,被一行代码所取代了,
// 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 的方式,解决了上述的,这里唯一需要知道的是,代码,
Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
等价于如下匿名接口类的实现,得到的就是一个实现了 Predicate 接口的匿名类实例;
new MyTest<Person>(){ @Override public boolean test(Person p){ return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE; } }