我对java中lambda表达式的看法是相当纠结的:
一个我这么想:lambda表达式降低了java程序的阅读体验。java程序一直不以表现力出众,正相反使Java流行的一个因素正是它的安全和保守——即使是初学者只要注意些也能写出健壮且容易维护的代码来。lambda表达式对开发人员的要求相对来说高了一层,因此也增加了一些维护难度。
另一个我这么想:作为一个码代码的,有必要学习并接受语言的新特性。如果只是因为它的阅读体验差就放弃它在表现力方面的长处,那么即使是三目表达式也有人觉得理解起来困难呢。语言也是在发展的,跟不上的就自愿被丢下好了。
我不愿意被丢下。不过非让我做出选择的话,我的决定还是比较保守的:没必要一定在java语言中使用lambda——它让目前Java圈子中的很多人不习惯,会造成人力成本的上升。如果非常喜欢的话,可以考虑使用scala。
不管怎样,我还是开始试着掌握Lambda了,毕竟工作中维护的部分代码使用了Lambda(相信我,我会逐步把它去掉的)。学习的教程是在Oracla – Java官网的相关教程。
——————————
假设目前正在创建一个社交网络应用。其中的一个特性是管理员可以对符合指定条件的会员执行某些操作,如发送消息。下面的表格详细描述了这个用例:
Field |
描述 |
名称 |
要执行的操作 |
主要参与者 |
管理员 |
前提条件 |
管理员登录系统 |
后置条件 |
只对符合指定标准的会员执行操作 |
主成功场景 |
1. 管理员对要执行操作的目标会员设置过滤标准; |
扩展 |
在选择执行操作前或者点击提交按钮前,管理员可以选择是否预览符合过滤标准的会员信息。 |
发生频率 |
一天中会发生许多次。 |
使用下面的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(); } } } |
这是一种很脆弱的方案,极有可能因为一点更新就导致应用无法运行。假如我们为Person类添加了新的成员变量或者更改了标准中衡量年龄的算法,就需要重写大量的代码来适应这种变化。再者,这里的限制也太过僵化了,比方说我们要打印年龄小于某个指定值的成员又该怎么做呢?再添加一个新方法printPersonsYoungerThan?这显然是一个笨方法。
方案二:创建更通用的方法
下面的方法较之printPersonsOlderThan适应性更好一些;这个方法打印了在指定年龄段内的会员信息:
1
2
3
4
5
6
7
8
|
public static void printPersonsWithinAgeRange( List<person> roster, int low, int high) { for (Person p : roster) { if (low <= p.getAge() && p.getAge() < high) { p.printPerson(); } } } |
此时又有新的想法了:如果我们要打印指定性别的会员信息,或者同时符合指定性别又在指定年龄段内的会员信息该怎么办呢?如果我们调整了Person类,添加了诸如友好度和地理位置这样的属性又该怎么办呢。尽管这样写方法要比printPersonsYoungerThan通用性更强一些,但是如果为每一种可能的查询都写一个方法也会导致代码的脆弱。倒不如把标准检查这一块代码给独立到一个新的类中。
方案三:在一个局部类中实现标准检查
下面的方法打印了符合检索标准的会员信息:
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(); } } } |
在程序里使用了一个CheckPerso对象tester对List参数roster中的每个实例进行校验。如果tester.test()返回true,就会执行printPerson()方法。为了设置检索标准,需要实现CheckPerson接口。
下面的这个类实现了CheckPerson并提供了test方法的具体实现。这个类中的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 ; } } |
要使用这个类,需要创建一个实例并触发printPersons方法:
1
2
|
printPersons( roster, new CheckPersonEligibleForSelectiveService()); |
现在的代码看起来不那么脆弱了——我们不需要因为Person类结构的变化而重写代码。不过这里仍有额外的代码:一个新定义的接口、为应用中每个搜索标准定义了一个内部类。
因为CheckPersonEligibleForSelectiveService实现了一个接口,所以可以使用匿名类,而不需要再为每种标准分别定义一个内部类。
方案四:使用匿名类实现标准检查
下面调用的printPersons方法中的一个参数是匿名类。这个匿名类的作用和方案三中的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表达式替换匿名类。
方案五:使用Lambda表达式实现标准检查
CheckPerson接口是一个函数式接口。所谓的函数式接口就是指任何只包含一个抽象方法的接口。(一个函数式接口中也可以有多个default方法或静态方法)。既然函数式接口中只有一个抽象方法,那么在实现这个函数式接口的方法的时候可以省略掉方法的方法名。为了实现这个想法,可以使用Lambda表达式替换匿名类表达式。在下面重写的printPersons方法中,相关的代码做了高亮处理:
1
2
3
4
5
6
|
printPersons( roster, (Person p) -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 ); |
在这里还可以使用一个标准的函数接口来替换CheckPerson接口,从而进一步简化代码。
方案六:在Lambda表达式中使用标准函数式接口
再来看一下CheckPerson接口:
1
2
3
|
interface CheckPerson { boolean test(Person p); } |
这是一个非常简单的接口。因为只有一个抽象方法,所以它也是一个函数式接口。这个抽象方法只接受一个参数并返回一个boolean值。这个抽象接口太过简单了,以至于我们会考虑是否有必要在应用中定义一个这样的接口。此时可以考虑使用JDK定义的标准函数式接口,可以在java.util.function包下找到这些接口。
在这个例子中,我们就可以使用Predicate<T>接口来替换CheckPerson。在这个接口中有一个boolean test(T t)方法:
1
2
3
|
interface Predicate<t> { boolean test(T t); } |
Predicate<T>接口是一个泛型接口。泛型类(或者是泛型接口)使用一对尖括号(<>)指定了一个或多个类型参数。在这个接口中只有一个类型参数。在使用具体类声明或实例化一个泛型类时,就会获得一个参数化类。比如说参数化类Predicate<Person>就是这样的:
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 ); |
有没有注意到,这里使用Predicate<Person>作为参数类型时并没有显式指定参数类型。这里并不是唯一适用lambda表达式的地方,下面的方案会介绍更多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(); } } } |
在这个方法中使用Predicate实例tester检查了roster中的每个Person实例。如果Person实例符合tester中定义的检查标准,将会触发Person实例的printPerson方法。
除了触发printPerson方法,满足tester标准的Person实例还可以执行其他的方法。可以考虑使用lambda表达式指定要执行的方法(私以为这个特性很好,解决了java中方法不能作为对象传递的问题)。现在需要一个类似printPerson方法的lambda表达式——一个只需要一个参数且返回为void的lambda表达式。记住一点:要使用lambda表达式,需要先实现一个函数式接口。在这个例子中需要一个函数式接口,其中只包含一个抽象方法,这个抽象方法有个类型为Person的参数,且返回为void。可以看一下JDK提供的标准函数式接口Consumer<T>,它有一个抽象方法void accept(T t)正好满足这个要求。在下面的代码中使用一个Consumer<T>的实例调用accept方法替换了p.printPerson():
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() ); |
如果我们想做的事情不只是打印会员信息,而是更多的事情,比如验证会员身份、获取会员联系方式等等。此时,我们需要一个有返回值方法的函数式接口。JDK的标准函数式接口Function<T,R>就有一个这样的方法R apply(T t)。下面的方法从参数mapper中获取数据,并在这些数据上执行参数block指定的行为:
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); } } } |
下面的代码获取了roster中适龄服兵役的所有会员的邮箱信息并打印出来:
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) ); |
方案八:多使用泛型
再来回顾一下processPersonsWithFunction方法。下面是这个方法的泛型版本,新方法在参数类型上要求更为宽容:
1
2
3
4
5
6
7
8
9
10
11
12
|
public static <x , Y> void processElements( Iterable<x> source, Predicate<x> tester, Function<x , Y> mapper, Consumer<y> block) { for (X p : source) { if (tester.test(p)) { Y data = mapper.apply(p); block.accept(data); } } } |
要打印适龄服兵役的会员信息可以像下面这样调用processElements方法:
1
2
3
4
5
6
7
8
|
processElements( roster, p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 , p -> p.getEmailAddress(), email -> System.out.println(email) ); |
在方法的调用过程中执行了如下行为:
从一个集合中获取对象信息,在这个例子里是从集合实例roster中获取Person对象信息。
过滤能够匹配Predicate实例tester的对象。在这个例子里,Predicate对象是一个lambda表达式,它指定了过滤适龄服兵役的条件。
将过滤后的对象交给一个Function对象mapper处理,mapper会为这个对象匹配一个值。在这个例子中Function对象mapper是一个lambda表达式,它返回了每个会员的邮箱地址。
由Consumer对象block为mapper匹配的值指定一个行为。在这个例子里,Consumer对象是一个lambda表达式,它的作用是打印一个字符串,也就是Function实例mapper返回的会员邮件地址。
方案九:使用将lambda表达式作为参数的聚集操作
下面的代码中使用了聚集操作来打印roster集合中适龄服兵役会员的邮件地址:
1
2
3
4
5
6
7
|
roster.stream() .filter( p -> p.getGender() == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25 ) .map(p -> p.getEmailAddress()) .forEach(email -> System.out.println(email)); |
分析下如上代码的执行过程,整理如下表:
行为 |
聚集操作 |
获取对象 |
Stream<E> stream() |
过滤匹配Predicate实例指定标准的对象 |
Stream<T> filter(Predicate<? super T> predicate) |
通过一个Function实例获取对象匹配的值 |
<R> Stream<R> map(Function<? super T,? extends R> mapper) |
执行Consumer实例指定的行为 |
void forEach(Consumer<? super T> action) |
表中的filter、map和forEach操作都是聚集操作。聚集操作处理的元素来自Stream,而非是直接从集合中获取(就是因为这示例程序中调用的第一个方法是stream())。Stream是一个数据序列。和集合不同,Stream并没有用特定的结构存储数据。相反的,Stream从一个特定的源获取数据,比如从集合获取数据,通过一个pipeline。pipeline是一个Stream操作序列,在这个例子中就是filter-map-forEach。此外,聚集操作通常采用lambda表达式作为参数,这也给了我们许多自定义的空间。