背景
通过对行为参数化的学习,我们了解了利用参数行为化来传递代码有利于应对不断变化的需求。但同时我们也看到,实现不同的筛选标准时写了大量模板类的代码(即使使用了匿名类的机制)。这类方式并不令人十分满意:代码十分的啰嗦,会严重影响程序员在实践中使用行为参数化的积极性。
Lambda管中窥豹
可以把Lambda表达式理解为简洁的表示一种可传递的匿名类的一种方式:它没有类和函数名称,但有参数列表、函数主体、以及返回值类型,可能还有一个可抛出的异常列表。Lambda表达式的出现,就是为鼓励程序员采用参数行为化的编程风格。接上回的案例,用Lambda表达式改造后的代码如下:
List<Student> firstGradeStudents = filterStudents(students, (Student s) -> {
return s.getGrade() == 1;
});
不得不承认,代码看起来比使用匿名类更加简洁明了了!甚至,我们还可以进一步的简化代码:
List<Student> firstGradeStudents = filterStudents(students, s -> s.getGrade() == 1);
Lambda的基本语法是:
(parameters) -> expression 或 (parameters) -> { statements; }
以下是一些Lambda的例子和使用案例
使用案例 | Lambda示例 |
---|---|
布尔表达式 | (Student s) -> s.getSex() == ‘W’ |
创建对象 | () -> new Student() |
消费一个对象 | (Student s) -> System.out.println(s.getSex()) |
组合两个值 | (int a, int b) -> a + b |
Lambda的语法及使用
我们到底在哪里可以使用Lambda表达式呢?上一个例子中,我们把Lambda表达式作为第二个参数直接传给了filter方法,除此之外,我们还可以把Lambda表达式赋值给一个函数式接口的变量。那所谓的函数式接口又为何物?
1. 函数式接口
一言以蔽之,函数式接口指的是只定义了一个抽象方法的接口。如:
Predicate<T>、Function<T, R>、Comparator<T>
等等都是函数式接口,常用注解@FunctionalInterface加以区别普通的接口。Lambda表达式允许程序员直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。我们也可用匿名类的方式来实现,但是稍显笨拙。下面的两端代码是等效的:
Predicate<Student> girlPredicate = s -> s.getSex() == 'W';
Predicate<Student> girlPredicate = new Predicate {
public boolean test(Student s) {
return s.getSex() == 'W';
}
};
2.函数描述符
函数式接口的抽象方法签名就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符。
Note: 我们在将Lambda表达式赋值给函数式接口变量的时候,Lambda表达式的签名一定要和函数式接口的抽象方法一样,否则就会报错(Student的Predicate接收一个Student类型的参数,返回boolean类型的值):
OK: Predicate<Student> girlPredicate = s -> s.getGrade() == 'W';
ERROR: Predicate<Student> predicate = s -> s.getGrade();
3.类型检查、类型推断
1. 类型检查
Lambda表达式的类型是从使用Lambda表达式的上下文推断出来的。上下文中Lambda表达式所需要的类型叫做目标类型。让我们通过一个例子来进行观察:
类型检查过程可以分解成如下步骤:
- 找出filterStudents方法的声明;
- 确定第二个参数的目标类型Predicate< Student >;
- Predicate< Student >是一个函数式接口,其抽象方法是test;
- 确定test抽象方法的函数描述符:接收一个Student,返回一个boolean;
- 检查Lambda表达式的签名是否与test的函数描述符一致;
有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要Lambda表达式的签名与函数式接口的函数描述符匹配。比如:
Callable<String> c = () -> "test";
Supplier<String> s = () -> "test";
特殊的void兼容规则:
如果一个Lambda的主体是一个语句表达式,那么它就和一个返回void的函数描述符匹配(前提是参数列表匹配)。
// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);
2. 类型推断
我们还可以进一步简化我们的代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来匹配我们的Lambda表达式,这也意味着编译器也可推断出适合的Lambda表达式签名,因为函数描述符可以通过目标类型来得到。这样,我们就可以省略Lambda表达式中的参数类型。比如:
Predicate<Student> p = s -> s.getGrade() == 1; // 当参数只有一个时,可以省略()
Comparator<Student> c = (s1, s2) -> s1.getGrade().compareTo(s2.getGrade());
Note:
有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择。
4. 方法引用
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。比如:
Comparator<Student> c = Comparator.comparing(Student::getGrade);
事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。其语法为:目 标引用 放 在 分隔符::前 ,方法 的 名 称放在 后 面。方法引用主要分为三类:
- 指向静态方法的方法引用(eg:Integer的parseInt方法, 写作Integer::parseInt);
- 指向类型任一实例的方法引用(eg:String的length方法,写作String::length);
- 指向现有对象的实例方法的方法引用(eg:你有一个局部变量student,写作student::getGrade);
第一、二中用法很好理解,对于第三种用法看起来可能就有点懵逼。其实第三种用法的场景是你有一个局部变量,现有一个需求是打印该对象的每个属性的值,打印函数将作为一个工具方法提供,如下:
public static void printStudent(Student student) {
printStudentAttribute(student::getGrade); // OK
printStudentAttribute(() -> student::getGrade); // OK
printStudentAttribute(Student::getTotalScore); // ERROR
}
public static void printStudentAttribute(Supplier<Object> supplier) {
System.out.println(supplier.get());
}
其实这种用法完全可以用传值替代,感觉没多大必要传一个函数式接口,反正在项目中至今我还未如此使用过。