英文原文,本文为转载的译文
假设你正在创建一个社交网络应用。你现在要开发一个可以让管理员对用户做各种操作的功能,比如搜索、打印、获取邮件等操作。假设社交网络应用的用户都通过Person
类表示:
public class Person {
public enum Sex {
MALE, FEMALE
}
private String name;
private LocalDate birthday;
private Sex gender;
private String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
假设社交网络应用的所有用户都保存在一个 List<Person>
的实例中。
我们先使用一个简单的方法来实现这个用例,再通过使用本地类、匿名内部类实现,最终通过lambda表达式做一个高效且简洁的实现。
方法1:创建一个根据某一特性查询匹配用户的方法
最简单的方式是创建几个函数,每个函数搜索指定的用户特征,比如searchByAge()
这种方法,下面的方法打印了年龄大于某特定值的所有用户:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) { //这里限定了搜索条件,经不起变化
p.printPerson();
}
}
}
这个方法是有潜在的问题的,如果引入一些变动(比如新的数据类型)这个程序会出错。假设更新了应用且变化了Person
类,比如使用出生年月代替了年龄;也有可能搜索年龄的算法不同。这样你将不到不再写许多API来适应这些变化。
方法2:创建一个更加通用的搜索方法
这个方法比起printPersonsOlderThan
更加通用;它提供了可以打印某个年龄区间的用户:
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类,添加其他属性,比如恋爱状态、地理位置呢?尽管这个方法比printPersonsOlderThan
方法更加通用,但是每个查询都创建特定的函数都是有可以导致程序不够健壮。你可以使用接口将特定的搜索转交给需要搜索的特定类中(面向接口编程的思想——简单工厂模式)。
方法3:在本地类中设定特定的搜索条件
下面的方法用类实现CheckPerson接口,重写接口的test方法设置搜索条件,使用时只需提供实现了CheckPerson接口的实例
//CheckPerson接口设置搜索条件
interface CheckPerson {
boolean test(Person p);
}
//CheckPersonEligibleForSelectiveService 类实现CheckPerson接口,制定具体搜索条件
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
//定义printPersons方法
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
//调用printPersons方法
printPersons(roster, new CheckPersonEligibleForSelectiveService());
尽管这个方式不那么脆弱——当Person
发生变化时你不需要重新更多方法,但是你仍然需要在添加一些代码:要为每个搜索标准创建一个本地类来实现接口。CheckPersonEligibleForSelectiveService
类实现了一个接口,你可以使用一个匿内部类替代本地类,通过声明新的内部类来满足不同的搜索。
方法4:在匿名内部类中指定搜索条件
下面的printPersons
函数调用的第二个参数是一个匿名内部类,这个匿名内部类过滤满足性别为男且年龄在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表达式替换匿名内部类,像下面介绍的这种。
方法5:通过Lambda表达式实搜索接口
CheckPerson
接口是一个函数式接口。接口中只有一个抽象方法的接口属于函数式接口(一个函数式接口也可能包换一个活多个默认方法或者静态方法)。由于函数式接口只包含一个抽象方法,你可以在实现该方法的时候省略方法的名字。因此你可以使用lambda表达式取代匿名内部类表达式,像下面这样调用:
printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
lambda表达式的语法后面会做详细介绍。你还可以使用标准的函数式接口取代CheckPerson
接口,这样会进一步减少代码量。
方法6:使用标准的函数式接口和Lambda表达式
CheckPerson
太过简单以至于没有必要在你应用中定义它。因此JDK中定义了一些标准的函数式接口,可以在java.util.function
包中找到。比如,你可以使用Predicate<T>
取代CheckPerson
。这个接口中只包含boolean test(T t)
方法。
interface Predicate<T> {
boolean test(T t);
}
Predicate<T>
是一个泛型接口,泛型需要在尖括号(<>)指定一个或者多个参数。这个接口中只包换一个参数T。当你声明或者通过一个真实的类型参数实例化泛型后,你将得到一个参数化的类型。比如,参数化后的类型Predicate<Person>
像下面代码所示:
//标准函数式接口
interface Predicate<Person> {
boolean test(Person t);
}
//定义参数包含函数式接口变量的方法
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
//将lambda表达式赋给函数式接口变量
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
这个不是使用lamdba表达式的唯一的方式。建议使用下面的其他方式使用lambda表达。
方法7:在应用中全都使用Lambda表达式
再来看看方法printPersonsWithPredicate
哪里还可以使用lambda表达式:这个方法检测roster
中的每个Person
实例是否满足tester
的标准。如果Person实例满足tester
中设定的标准,那么Person
实例的信息将会被打印出来。
public static void printPersonsWithPredicate(
List<Person> roster, Predicate<Person> tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
你可以指定一个不同的动作来执行打印满足tester
中定义的搜索条件的Person
实例。你可以指定这个动作是一个lambda表达式。假设你想要一个功能和printPerson
一样的lambda表示式(一个参数、返回void),你需要实现一个函数式接口。在这种情况下,你需要一个包含一个只有一个Person类型参数和返回void的函数式接口。Consumer<T>
接口包换一个void accept(T t)
函数,它符合上述需求。下面的函数使用 Consumer<Person>
调用accept()
从而取代了p.printPerson()
的调用。
//定义方法
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
//为Predicate和Consumer接口变量赋lambda
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);
如果你想对用户的信息进行更多处理而不止打印出来,假设你想验证成员的个人信息或者获取他们的联系人的信息,在这种情况下,你需要一个有返回值的抽象函数的函数式接口。Function<T,R>
接口包含了R apply(T t)
方法,有一个参数和一个返回值。下面的方法从roster
中获取符合搜索条件的用户的邮箱地址,并将地址打印出来:
//定义方法
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);
}
}
}
//调用方法
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
方法8:使用泛型使之更加通用
再处理processPersonsWithFunction
函数,下面的函数可以接受包含任何数据类型的集合:
public static <X, Y> void processElements( //Person换成X;String换成Y
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(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
该方法的调用只要执行了下面动作:
- 从集合中获取对象,在这个例子中它是包换
Person
实例的roster
集合。roster
是一个List类型,同时也是一个Iterable类型。 - 过滤符合
Predicate
数据类型的tester
的对象。在这个例子中,Predicate对象是一个指定了符合搜索条件的lambda表达式。 - 使用
Function
类型的mapper映射每个符合过滤条件的对象。在这个例子中,Function对象时要给返回用户的邮箱地址。 - 对每个映射到的对象执行一个在
Consumer
对象块中定义的的动作。在这个例子中,Consumer对象时一个打印Function对象返回的电子邮箱的lamdba表达式。
你可以通过一个聚合操作取代上述操作。
方法9:使用lambda表达式作为参数的合并操作
下面的例子使用了聚合操作,打印出了符合搜索条件的用户的电子邮箱:
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));
下面的表映射了processElements
函数执行操作和与之对应的聚合操作
processElements动作 | 聚合操作 |
---|---|
获取对象源 | Stream stream() |
过滤符合Predicate对象(lambda表达式)的实例 | Stream filter(Predicate<? super T> predicate) |
使用Function对象映射符合过滤标准的对象到一个值 | Stream map(Function<? super T,? extends R> mapper) |
执行Consumer对象(lambda表达式)设定的动作 | void forEach(Consumer<? super T> action) |
filter
,map
和forEach
是聚合操作。聚合操作是从stream
中处理各个元素的,而不是直接从集合中(这就是为什么第一个调用的函数是stream()
)。steam是对各个元素进行序列化操作。和集合不同,它不是一个储存数据的数据结构。相反地,stream加载了源中的值,比如集合通过pipeline
将数据加载到stream中。pipeline
是stream的一种序列化操作,这个例子中的就是filter- map-forEach
。还有,聚合操作通常可以接收一个lambda表达式作为参数,这样你可自定义需要的动作