宇宙正在为我敞开大门,给我带来机会。我不会在伟大的边缘崩溃。我抓住来的机会,自信地走过那些门。
什么是 Lambda 表达式
匿名类可用于表示不同的行为,例如“过滤那些年龄在35岁以上的员工”、“过滤985、211的员工”,但这 仍然不够简洁,我们需要为这些过滤条件声明对应的类,只使用它们中的接口进行条件判断。
Lambda 表达式是用来 替代匿名类 的一种精简的表达形式,它降低了只有一个接口匿名类的复杂程度,直接描述行为本身,能够提升代码可读性和简洁性。
Lambda 表达式的名字来源于希腊字母 λ
,它鼓励人们采用行为参数化的方式简明传递行为。
Lambda 表达式的基本语法
Lambda 表达式的由三部分组成
- 参数: 带有类型声明的参数列表
- 箭头: ->
- 表达式主体: 即函数体,可以有返回值,也可以没有返回值
以“筛选年龄大于35岁的员工”这一用例来讲,5行
的代码可以直接精简成1行
。
// 筛选资深员工,匿名类写法
Comparator<Employee> byAge = new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
return e1.getAge().compareTo(e2.getAge());
}
}
// Lambda 写法
Comparator<Employee> byAge = (Employee e1, Employee e2) -> e1.getAge().compareTo(e2.getAge());
Lambda 表达式的几种变体写法
分成左右两侧来讨论,如下图所示:
为什么要用 Lambda 表达式
- 代码 简洁性
- 与 行为参数化 兼容良好
- 与 流式处理 兼容性良好
Lambda 表达式的典型应用场景
所谓函数式接口,是指只定义一个抽象方法的接口。
以下是 JDK 里常见的函数式接口。
// JDK 常见函数式接口
// java.util.Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
}
// java.lang.Runnable
public interface Runnable {
void run();
}
所有函数式接口的应用场景,都可以用 Lambda 表达式进行替代。
public static void process(Runnable r){
r.run();
}
process(() -> System.out.println("Hello World 3")); // 使用 Lambda 表达式作为 Runnable 类型的参数
Lambda 表达式的应用场景总结
虽然 Lambda 表达式可以用在任何函数式接口场景下,但仍然有必要对其典型应用场景进行总结:
- 从处理与对象的关系角度: 消费一个对象,生成一个对象。
- 在消费一个对象时: 生成布尔值,提取任意信息。
- 在处理多个对象时: 进行比较,组合两个值。
使用案例 | Lambda 示例 |
---|---|
消费一个对象 | (Employee e) -> { System.out.println(e.name) } |
生成一个对象 | () -> new Employee("Davlid") |
生成布尔值 | (Employee e) -> e.age > 35 |
提取任意信息 | (Employee e) -> e.salary |
进行比较 | (Employee e1, Employee e2) -> e1.age > e2.age |
组合两个值 | (Employee e1, Employee e2) -> e1.salary + e2.salary |
补充知识:java.util.function 包引入的几个函数式接口
java.util.function
包里面新引入了 Consumer
、Function
、Predicate
等函数式接口,分别对应 消费一个对象
、由对象进行运算并返回结果
、由对象进行运算并返回 Boolean 结果
。
使用 Consumer
java.util.function.Consumer<T>
定义了名为 accept
的抽象方法,接收泛型 T
的对象,没有返回(或者说返回 void
)。用于 访问类型 T 的对象,并对其执行某些操作 的场景。
以下代码对列表创建了 forEach
函数,在遍历列表时执行特定操作,例如打印列表中的每一个元素。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
// 定义 forEach
public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T i: List) {
c.accept(i);
}
}
// 执行语句,遍历打印列表中的每一个元素
forEach(
Arrays.asList(1,2,3,4,5),
(Integer i) -> System.out.println(i)
);
使用 Function
java.util.function.Function<T, R>
作为 SAM,定义了名为 apply
的函数,用于 接受泛型T的对象,生成泛型R的对象,可以理解为 对象映射。
接下来利用它创建了 map
函数,用于对列表中的元素执行某项操作,例如计算 String 字符串长度。
@FunctionalInterface
public interface Function<T,R> {
R apply(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T,R> f) {
List<R> result = new ArrayList<>();
for (T t: list) {
result.add(f.apply(t));
}
return result;
}
// 计算每个字符串长度,结果输出 [7,2,6]
List<Integer> l = map(
Arrays.asList("lambdas", "in", "action"),
(String s) -> s.length()
);
使用 Predictable
java.util.function.Predictable<T>
定义了 test
函数,由对象计算出布尔返回值,用于条件判断。
接口声明为泛型,在实际使用时,可以应用在任何需要的类型上。这里使用 Predicate
接口创建一个名为 filter
的函数,实现对列表的过滤操作。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
// 实现 test 函数,String -> Boolean
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
// 在过滤器中使用 Predicate 行为(而非对象)
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
函数式接口的基本类型特化
在上文介绍的三个泛型函数式接口 Consumer<T>
、Function<T,R>
和 Predicate<T>
中,均使用了 泛型 作为参数类型声明。在 Java 中存在两种对象:引用类型(例如 Byte、Integer、Object、List)和基本类型(例如 int、double、byte、char)。而泛型只能绑定到引用类型,这是由泛型内部实现方式决定的。因此,如果想要对基础类型的数据执行函数式操作,就必然伴随着装箱(boxing)行为,装箱和拆箱是自动完成的,但这并不意味着在使用它们时,我们没有付出代价。
装箱后的值本质上是把基础类型包裹起来,并保存在堆里,因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
对此,Java 8 提供了针对基本类型特化后的函数式接口,例如 DoublePredicate
、IntConsumer
、LongBinaryOperator
、IntFunction
等。这些特化接口列举如下:
函数式接口 | 函数描述符 | 基本类型特化 |
---|---|---|
Predicate | T->boolean | IntPredicate, LongPredicate, DoublePredicate |
Consumer | T->void | IntConsumer, LongConsumer, DoubleConsumer |
Function<T,R> | T->R | IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction |
Supplier | () -> T | BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
UnaryOperator | T->T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator |
BinaryOperator | (T,T)->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator |
BiPredicate<L,R> | (L,R)->boolean | |
BiConsumer<T, U> | (T,U)->void | ObjIntConsuer, ObjLongConsumer, ObjDoubleConsumer |
BiFunction<T,U,R> | (T,U)->R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> |
Lambda 表达式的类型处理
类型检查
编译器在判断一个 Lambda 表达式使用是否合理时,会从函数签名来进行匹配,Lambda 表达式的输入参数、输出参数应当与函数式接口相对应。这样才是一个正确使用的 Lambda 表达式。
同一个表达式可用于不同的函数式接口
例如以下三个函数式接口,接收两个 Employee
类型的入参,返回 boolean
结果,它们的实现对象可以是完全一样的 Lambda 表达式,很神奇。
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
void 兼容规则,无视返回值
即使一个语句的执行结果有返回值,也可以将其套用在不需要返回值的 Lambda 语境中,例如在使用对象时的 Consumer,它需要一个 (T -> void)
表达式,我们用 list.add(element)
会返回 true or false 的插入结果,编译器会直接无视其结果。
以下两种写法都是正确的。
// Predicate 需要一个 boolean
Predicate<String> p = s -> list.add(s);
// Consumer 只需要 void
Consumer<String> b = s -> list.add(s);
类型推断:简化 Lambda 表达式的类型声明
编译器可以从上下文推断出,你写下的 Lambda 表达式适用于什么样的函数式接口,这意味着它可以推断出适合 Lambda 的签名,从而在编码时省略参数类型的声明。
例如我们要找出列表里的资深员工。
// 完整写法
List<Employee> seniorEmployees = filter(employeeList, (Employee e) -> e.age > 35)
// 简化写法
List<Employee> seniorEmployees = filter(employeeList, e -> e.age > 35)
迄今为止,我们看到的 Lambda 表达式箭头左侧的参数,都是由括号 () 包围的。而当 Lambda 仅有一个参数时,借助类型推断,该参数两边的括号也可以省略。
方法引用
方法引用与 Lambda 相关,但又不完全一致。同样是 Java 8 引入的新语法,作用于行为参数化的场景,方法引用更加易读,感觉也更自然。
例如,使用方法引用对员工的工作经验进行排序,改进 Lambda 的写法:
// 使用 Lambda
employees.sort((Employee e1, Employee e2) -> e1.getExp().compareTo(e2.getExp()));
// 使用方法引用,借助 java.util.Comparator.comparing
employees.sort(comparing(Employee::getExp));
方法引用与 Lambda 之间的关系
使用方法引用的场景,都可以用 Lambda 代替,那么,我们为什么还需要方法引用呢?相比与 Lambda 描述具体的行为,方法引用为这种行为赋予“名称”,也就是方法名(函数名)。
- Lambda 描述执行什么操作,是具体信息
- 方法引用描述操作的名称,可以是抽象信息
方法引用本质上是根据已有的方法实现(我个人更习惯于使用“函数”而非“方法”来称呼 function/method),来创建对应的 Lambda 表达式。通过显式地指明方法名称,代码的可读性会得到提升。
可以把方法引用看做针对仅仅涉及单一方法的 Lambda 的语法糖。
方法引用的格式
目标引用
+ 分隔符::
+ 方法名
注意方法引用只需要指明方法名,不需要写括号,因为实际上并未对方法进行调用,无需括号。
方法引用的一些例子如下表:
Lambda | 等效方法引用 |
---|---|
(Employee e) -> e.getExp() | Employee::getExp |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::pringln |
方法引用的三种构建方式
根据分隔符::
左侧的 引用目标 的不同,方法引用有三种构建方式。
引用目标 | 例子 | 理解 |
---|---|---|
静态方法 | Integer::parseInt | 调用静态方法处理某参数 |
类的实例方法 | String::length | 该对象是 Lambda 的一个参数 |
对象实例方法 | someEmployee::getAge | 调用外部对象某方法 |
以上三种方法引用可以与 Lambda 表达式互相转化。
参考资料
- Java 8 in Action