java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaEE(j2ee), JavaME(j2me), JavaSE(j2se))的总称。
让我们从最简单的例子开始,来学习如何对一个 string 列表进行排序。我们首先使用 Java 8 之前的方法来实现
我对 java 中 lambda 表达式的看法是相当纠结的:
一个我这么想:lambda 表达式降低了 java 程序的阅读体验。java 程序一直不以表现力出众,正相反使 Java 流行的一个因素正是它的安全和保守——即使是初学者只要注意些也能写出健壮且容易维护的代码来。lambda 表达式对开发人员的要求相对来说高了一层,因此也增加了一些维护难度。
另一个我这么想:作为一个码代码的,有必要学习并接受语言的新特性。如果只是因为它的阅读体验差就放弃它在表现力方面的长处,那么即使是三目表达式也有人觉得理解起来困难呢。语言也是在发展的,跟不上的就自愿被丢下好了。
我不愿意被丢下。不过非让我做出选择的话,我的决定还是比较保守的:没必要一定在 java 语言中使用 lambda——它让目前 Java 圈子中的很多人不习惯,会造成人力成本的上升。如果非常喜欢的话,可以考虑使用 scala。
不管怎样,我还是开始试着掌握 Lambda 了,毕竟工作中维护的部分代码使用了 Lambda(相信我,我会逐步把它去掉的)。学习的教程是在 Oracla – Java 官网的相关教程。
——————————
假设目前正在创建一个社交网络应用。其中的一个特性是管理员可以对符合指定条件的会员执行某些操作,如发送消息。下面的表格详细描述了这个用例:
Field | 描述 |
名称 | 要执行的操作 |
主要参与者 | 管理员 |
前提条件 | 管理员登录系统 |
后置条件 | 只对符合指定标准的会员执行操作 |
主成功场景 | 1. 管理员对要执行操作的目标会员设置过滤标准; 2. 管理员选择要执行的操作; 3. 管理员点击提交按钮; 4. 系统找到符合指定标准的会员; 5. 系统对符合指定标准的会员执行预先选择的操作。 |
扩展 | 在选择执行操作前或者点击提交按钮前,管理员可以选择是否预览符合过滤标准的会员信息。 |
发生频率 | 一天中会发生许多次。 |
使用下面的 Person 类来表示社交网络中的会员信息:
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 表达式的强大和高效。可以在这里找到完整的代码。
方案一:一个个地创建查找符合指定标准的会员的方法
这是实现前面提到的案例最简单粗糙的方案了:就是创建几个方法、每个方法校验一项标准(比如年龄或是性别)。下面的一段代码校验了年龄大于一个指定值的情况:
public static void printPersonsOlderThan(List < person > roster, int age) {
for (Person p: roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
这是一种很脆弱的方案,极有可能因为一点更新就导致应用无法运行。假如我们为 Person 类添加了新的成员变量或者更改了标准中衡量年龄的算法,就需要重写大量的代码来适应这种变化。再者,这里的限制也太过僵化了,比方说我们要打印年龄小于某个指定值的成员又该怎么做呢?再添加一个新方法 printPersonsYoungerThan?这显然是一个笨方法。
方案二:创建更通用的方法
下面的方法较之 printPersonsOlderThan 适应性更好一些;这个方法打印了在指定年龄段内的会员信息:
public static void printPersonsWithinAgeRange(
List roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
此时又有新的想法了:如果我们要打印指定性别的会员信息,或者同时符合指定性别又在指定年龄段内的会员信息该怎么办呢?如果我们调整了 Person 类,添加了诸如友好度和地理位置这样的属性又该怎么办呢。尽管这样写方法要比 printPersonsYoungerThan 通用性更强一些,但是如果为每一种可能的查询都写一个方法也会导致代码的脆弱。倒不如把标准检查这一块代码给独立到一个新的类中。
方案三:在一个局部类中实现标准检查
下面的方法打印了符合检索标准的会员信息:
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 岁之间。
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
要使用这个类,需要创建一个实例并触发 printPersons 方法:
printPersons(
roster, new CheckPersonEligibleForSelectiveService());
现在的代码看起来不那么脆弱了——我们不需要因为 Person 类结构的变化而重写代码。不过这里仍有额外的代码:一个新定义的接口、为应用中每个搜索标准定义了一个内部类。
因为 CheckPersonEligibleForSelectiveService 实现了一个接口,所以可以使用匿名类,而不需要再为每种标准分别定义一个内部类。
方案四:使用匿名类实现标准检查
下面调用的 printPersons 方法中的一个参数是匿名类。这个匿名类的作用和方案三中的 CheckPersonEligibleForSelectiveService 类一样:都是过滤性别为男且年龄在 18 和 25 岁之间的会员。
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 方法中,相关的代码做了高亮处理:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
在这里还可以使用一个标准的函数接口来替换 CheckPerson 接口,从而进一步简化代码。
方案六:在 Lambda 表达式中使用标准函数式接口
再来看一下 CheckPerson 接口:
interface CheckPerson {
boolean test(Person p);
}
这是一个非常简单的接口。因为只有一个抽象方法,所以它也是一个函数式接口。这个抽象方法只接受一个参数并返回一个 boolean 值。这个抽象接口太过简单了,以至于我们会考虑是否有必要在应用中定义一个这样的接口。此时可以考虑使用 JDK 定义的标准函数式接口,可以在 java.util.function 包下找到这些接口。
在这个例子中,我们就可以使用 Predicate<T> 接口来替换 CheckPerson。在这个接口中有一个 boolean test(T t) 方法:
interface Predicate < t > {
boolean test(T t);
}
Predicate<T> 接口是一个泛型接口。泛型类(或者是泛型接口)使用一对尖括号(<>)指定了一个或多个类型参数。在这个接口中只有一个类型参数。在使用具体类声明或实例化一个泛型类时,就会获得一个参数化类。比如说参数化类 Predicate<Person> 就是这样的:
interface Predicate < person > {
boolean test(Person t);
}
在这个参数化类中有一个与 CheckPerson.boolean test(Person p) 方法的参数和返回值都一致的方法。因此就可以同如下方法所演示的一样使用 Predicate<T> 接口来替换 CheckPerson 接口:
public static void printPersonsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
然后使用下面的代码就可以像方案三中一样筛选适龄服兵役的会员了:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
有没有注意到,这里使用 Predicate<Person> 作为参数类型时并没有显式指定参数类型。这里并不是唯一适用 lambda 表达式的地方,下面的方案会介绍更多 lambda 表达式的用法。
方案七:在整个应用中使用 lambda 表达式
再来看一下 printPersonsWithPredicate 方法,考虑是否可以在这里使用 lambda 表达式:
public static void printPersonsWithPredicate(
List roster, Predicate 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():
public static void processPersons(
List roster,
Predicate tester,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
对应这里,可以使用如下代码筛选适龄服兵役的会员:
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 指定的行为:
public static void processPersonsWithFunction(
List roster,
Predicate tester,
Function<person , string> mapper,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
下面的代码获取了 roster 中适龄服兵役的所有会员的邮箱信息并打印出来:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方案八:多使用泛型
再来回顾一下 processPersonsWithFunction 方法。下面是这个方法的泛型版本,新方法在参数类型上要求更为宽容:
public static <x , Y> void processElements(
Iterable source,
Predicate tester,
Function<x , Y> mapper,
Consumer block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}
要打印适龄服兵役的会员信息可以像下面这样调用 processElements 方法:
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 集合中适龄服兵役会员的邮件地址:
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 表达式作为参数,这也给了我们许多自定义的空间。
来源: http://www.phperz.com/article/17/1114/359977.html