我们通常在调用一个方法时,在参数列表中定义一个匿名内部类,向方法中传递一个代码块,用来指定个性化的行为,如:
Collections.sort(list,
new Comparator<Person>(){
@Override
public int compareTo(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
});
但是这样使用匿名内部类显得比较繁琐,要写很多无用代码,不够简洁,因此可以考虑使用Lamda表达式。
Lamda的典型用例
例如,你想开发一个社交软件,该社交软件支持管理员向满足某些条件的用户发送一条消息,下面的用例表详细描述了这个功能:
Field | Description |
---|---|
用例名称 | 对满足条件的用户执行某种操作 |
角色 | 管理员 |
前置条件 | 管理员已登录 |
后置条件 | 操作在满足条件的用户上执行成功 |
正常流程 |
|
扩展流程 | 1a. 管理员可以预览在提交操作之前的系统状态和提交之后系统将处于什么状态 |
执行频率 | 一天会执行很多次 |
社交软件中的用户数据结构如下:
public class Person {
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
社交软件中所有用户都是存储在List<Person> roster
中,下面将通过循序渐进的方式演示Lamda表达式是如何简化局部类和匿名内部类的使用的
Approach 1:查询满足某一特点的用户
一个比较简单的做法是,我们写一个方法,只过滤某一条件的用户(例如,找出所有的男性用户),下面的方法用于找出年龄超过指定值的用户:
public static void printPersonsOlderThan(List<Person> roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}
考虑到你可能会更具年龄范围筛选用户,因此你可以提供一个更加通用的方法:
public static void printPersonsWithinAgeRange(
List<Person> roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}
这种方式缺点非常明显,如果你是为别人提供API,那么你就会提供很多筛选的接口,一旦Person类增加新的字段,那么也就会增加新的筛选逻辑,那你必须提供这些筛选逻辑,使用你API的人才能够,进行相应的筛选,这样会导致扩展性不好。
Approach 2:使用接口抽象过滤规则
下面的程序打印出满足筛选条件的所有用户:
public static void printPersons(
List<Person> roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}
接口CheckPerson
定义了过滤规则,test
方法检查Person
对象是否满足条件
interface CheckPerson {
boolean test(Person p);
}
如果我们要找出年龄在18至25岁的男性,只需要实现CheckPerson
接口的test
方法就可以实现过滤:
class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}
printPersons(roster, new CheckPersonEligibleForSelectiveService());
API不需要提供过滤规则的实现,而是让使用API的人提供实现,降低代码的耦合度
Approach 3:使用匿名内部类简化代码
printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
这段代码和上面的代码大致一样,只是简化了一些代码,如果CheckPersonEligibleForSelectiveService
这个过滤规则只有一个地方使用,那么就没有必要将它定义成一个类,直接使用匿名内部内更加紧凑。
Approach 4 使用Lamda表达式
如果一个接口中只声明了一个方法(public abstract
修饰的方法),那么这个接口就被称为Function Interface
,因此当使用内部类时,如果内部类实现的接口是Function Interface
,我们就可以省略方法声明的代码,直接提供方法的实现,如下:
printPersons(
roster,
(Person p)->p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25);
Approach 5 使用标准Function Interface
为了避免向CheckPerson这种接口在系统中泛滥,因此JDK中提供了一系列标准的Function Interface
位于包java.util.function
下,我们可以利用Predicate<T>
接口来改写上面的代码,下面是Predicate<T>
的声明:
interface Predicate<T> {
boolean test(T t);
}
改写后的代码:
printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
Approach 6 在其他地方使用Lamda表达式
为了使程序更加通用,我们允许使用者任意指定要执行的操作,因此可以这样声明方法:
public static void processPersons(
List<Person> roster,
Predicate<Person> tester,
Consumer<Person> block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}
Consumer<T>
是JDK为我们提供的又一个Function Interface
,下面是接口的声明:
public interface Comsumer<T> {
void accept(T t);
}
因此通过这个接口的accept方法可以实现操作的自定义,可以使用Lamda表达式来调用上面的代码:
processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p->printPerson());
如果你不想打印用户信息,而是想打印用户的邮件地址或者打印用户的名字就可以了,这怎么写呢?这个时候需要用到Function<T, R>
这个接口,这也是一个标准接口,包含一个apply方法,它允许调用方返回关键对象
public static void processPersonsWithFunction(
List<Person> roster,
Predicate<Person> tester,
Function<R, T> mapper,
Consumer<String> block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}
使用Lamda表达式来调用上面的方法:
processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);
Lamda表达式语法
Lamda表达式语法由下面几个部分组成:
- 圆括号包裹的参数列表,如果只有一个参数,则可以去掉圆括号,下面是使用多个参数的例子:
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " +
myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " +
myApp.operateBinary(20, 10, subtraction));
}
}
- 箭头(->)
- body,可以包含一条语句
p->p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
java会计算并返回表达式的值,如果是语句块的话,需要用花括号包裹:
p->{
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
调用一个返回值为void的方法时,不需要花括号包裹:
email->System.out.println(email)