Lambda表达式
Lambda概述
lambda表达式没有名称,但是它有参数列表,函数体和返回类型,还可能包含一个异常列表。
- 匿名,lambda表达式不像方法一样有一个显式的名字
- 函数,lambda表达式不像方法一样关联到一个特定的类
- 传递,lambda表达式可以像函数参数一样传递,或者保存到变量里面
- 简洁,不需要像匿名类一样写很多样板代码
lambda基本语法有2种
|
|
和
|
|
用例 | lambda示例 |
---|---|
布尔表达式 | (List<String> list) -> list.isEmpty() |
创建对象 | () -> new Apple(10) |
消费对象 | (Apple a) -> { System.out.println(a.getWeight()); } |
从对象中提取属性 | (String s) -> s.length() |
合并两个值 | (int a, int b) -> a * b |
比较两个对象 | (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
哪里可以使用lambda
你可以在函数接口上下文中使用lambda表达式
函数接口
简单地说,函数接口就是只有一个抽象方法的接口,比如:
|
|
注意,接口现在可以有默认方法。如果一个接口有许多默认方法,只要它只有一个抽象方法,那么它仍然是一个函数接口。
lambda表达式可以为函数接口的抽象方法提供实现,并将整个表达式看做函数接口的一个实例。
函数描述符
函数接口的抽象方法的签名描述了lambda表达式的签名,我们称这个抽象方法为函数描述符。我们使用一个特殊的标记来描述函数描述符,比如() -> void表示参数为空并返回void的函数。
只要lambda表达式具有与函数接口的抽象方法相同的签名,就可以将lambda表达式赋值给变量或传递给参数为函数接口的方法。
下面这个lambda表达式合法:() -> System.out.println(“This is awesome”);
System.out.println
不是一个表达式,为什么不需要用花括号括起来? 原来,在Java语言规范中定义了一个用于void方法调用的特殊规则,void方法调用不需要使用花括号。
@FunctionalInterface用于指示一个接口是函数接口。如果使用@FunctionalInterface定义一个不是函数接口的接口,编译器会返回一个警告。@FunctionalInterface不是强制性的,但是使用它定义函数接口是个好习惯。
使用函数接口
Predicate<T>
java.util.function.Predicate<T>
定义如下:
|
|
Consumer<T>
java.util.function.Consumer<T>
定义如下:
|
|
Function<T, R>
java.util.function.Function<T, R>
定义如下:
|
|
Java 8同样添加了特殊版的函数接口以避免基础类型的autoboxing:
|
|
常用函数接口表
函数接口 | 函数描述符 | 特殊版本函数接口 |
---|---|---|
Predicate<T> | T -> boolean | IntPredicate LongPredicate DoublePredicate |
Consumer<T> | T -> void | IntConsumer LongConsumer DoubleConsumer |
Function<T, R> | T -> R | IntFunction<R> IntToDoubleFunction IntToLongFunction LongFunction<R> LongToDoubleFunction LongToIntFunction DoubleFunction<R> DoubleToIntFunction DoubleToLongFunction ToIntFunction<T> ToDoubleFunction<T> ToLongFunction<T> |
Supplier<T> | () -> T | BooleanSupplier IntSupplier LongSupplier DoubleSupplier |
UnaryOperator<T> | T -> T | IntUnaryOperator LongUnaryOperator DoubleUnaryOperator |
BinaryOperator<T> | (T, T) -> T | IntBinaryOperator LongBinaryOperator DoubleBinaryOperator |
BiPredicate<T, U> | (T, U) -> boolean | |
BiConsumer<T, U> | (T, U) -> void | ObjIntConsumer<T> ObjLongConsumer<T> ObjDoubleConsumer<T> |
BiFunction<T, U, R> | (T, U) -> R | ToIntBiFunction<T, U> ToLongBiFunction<T, U> ToDoubleBiFunction<T, U> |
注意这些函数接口都不允许抛出checked异常。如果你需要lambda表达式抛出异常,你可以定义自己的函数接口声明checked异常,或者在lambda表达式函数体中catch异常。
类型检查,类型推断与限制
类型检查
lambda表达式的类型是从使用lambda表达式的上下文中推导出来的。lambda表达式上下文中期待的类型称为目标类型。
同样的lambda不同的函数接口
由于目标类型的原因,如果不同的函数接口具有兼容的抽象方法签名,相同的lambda表达式可以关联到它们。
|
|
特殊的void兼容规则
如果lambda函数体是一个表达式语句,则它和返回void的函数描述符兼容。比如:
// Predicate has a boolean return
Predicate<String> p = (String s) -> list.add(s);
// Consumer has a void return
Consumer<String> b = (String s) -> list.add(s);
类型推断
Java编译器可以推断出lambda表达式关联的函数接口,意味着它也能够推导出相应的抽象函数的签名。好处是Java编译器知道lambda表达式的参数类型,因此lambda表达式可以忽略掉参数类型:
|
|
使用局部变量
lambda表达式允许使用自由变量(不是参数且定义在外部作用域),就像匿名类一样。
|
|
Lambda可以不受限制地捕获实例变量和静态变量。但是当捕获局部变量时,必须显式地声明它们为final或有效的final。Lambda表达式可以捕获只分赋值一次的局部变量。下面的代码将编译不了:
|
|
局部变量的限制
实例变量和局部变量在实现方式上有一个关键区别。实例变量存储在堆上,而局部变量存储在堆栈上。如果lambda可以直接访问局部变量,并且lambda在线程中使用,那么使用lambda的线程可以在分配变量的线程释放变量之后尝试访问该变量。因此Java将对自由变量的访问实现为对它的副本的访问,而不是对原始变量的访问。
方法引用
方法引用可以看作是只调用特定方法的lambda表达式的简写,可以将方法引用看作lambda的语法糖。
Lambda | 等价方法引用 |
---|---|
(Apple apple) -> apple.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpStack() | Thread.currentThread()::dumpStack |
(str, i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
(String s) -> this.isValidName(s) | this::isValidName |
构造方法引用
如下图:
构造函数引用
使用类名和关键字new创建构造函数引用:ClassName::new
,类似静态方法引用。比如,如果有不带参数的构造函数
|
|
等价于
|
|
如果有带一个参数的构造函数,则满足Function函数接口:
|
|
如果有带亮个参数的构造函数,则满足BiFunction函数接口:
|
|
组合lambda表达式的有用方法
组合Comparators
静态方法Comparator.comparing
返回一个基于函数的Comparator,该函数提取一个关键字进行比较,如下所示:
|
|
反向比较
reversed
方法反转指定Comparator的顺序:
|
|
链接Comparators
|
|
组合Predicates
谓词接口包含三个方法,可以重用现有的谓词来创建更复杂的谓词:negate
、and
和or
。
|
|
注意方法的优先级是从左到右。
组合Functions
Function接口提供2个方法用于组合lambda表达式:andThen
和compose
。
|
|