识别 Lambda 表达式的类型
在 Java 语言中,一切都有一个类型,这个类型在编译时是已知的。 所以总是可以找到 lambda 表达式的类型。 它可以是变量的类型、字段的类型、方法参数的类型或方法的返回类型。
lambda 表达式的类型有一个限制:它必须是一个函数式接口。 所以没有实现函数式接口的匿名类不能写成 lambda 表达式。
什么是函数式接口的完整定义有点复杂。 此时您需要知道的是,函数式接口是一个只有一个 的接口 抽象 方法 。
您应该知道,从 Java SE 8 开始,接口中允许使用具体方法。 它们可以是实例方法,在这种情况下,它们被称为 默认方法 ,它们可以是静态方法。 这些方法不算数,因为它们不是 抽象 方法。
我需要添加注释吗
@FunctionalInterface
在一个界面上使其发挥作用?不,你没有。 此注释可帮助您确保您的界面确实可以正常工作。 如果将此注释放在不是功能接口的类型上,则编译器将引发错误。
函数式接口示例
让我们看一些取自 JDK API 的示例。 我们刚刚从源代码中删除了注释。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
这 Runnable
接口确实是函数式的,因为它只有一个抽象方法。 这 @FunctionalInterface
注释已添加为帮助程序,但它不是必需的。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
// the body of this method has been removed
}
}
这 Consumer
接口也是函数式的:它有一个抽象方法和一个默认的、具体的方法,不算数。 再次,该 @FunctionalInterface
不需要注释。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
// the body of this method has been removed
}
default Predicate<T> negate() {
// the body of this method has been removed
}
default Predicate<T> or(Predicate<? super T> other) {
// the body of this method has been removed
}
static <T> Predicate<T> isEqual(Object targetRef) {
// the body of this method has been removed
}
static <T> Predicate<T> not(Predicate<? super T> target) {
// the body of this method has been removed
}
}
这 Predicate
接口稍微复杂一些,但它仍然是一个功能接口:
- 它有一个抽象方法
- 它有三个不计入的默认方法
- 它有两个静态方法,两者都不算。
寻找正确的实施方法
此时,您已经确定了需要编写的 lambda 表达式的类型,好消息是:您已经完成了最难的部分:其余部分非常机械且更容易完成。
lambda 表达式是此功能接口中唯一抽象方法的实现。 所以找到合适的方法来实现只是找到这个方法的问题。
您可以花一点时间在上一段的三个示例中查找它。
为了 Runnable
界面是:
public abstract void run();
For the Predicate
interface it is:
boolean test(T t);
And for the Consumer
interface it is:
void accept(T t);
Implementing the Right Method with a Lambda Expression
编写实现的第一个 Lambda 表达式 Predicate<String>
现在是最后一部分:编写 lambda 本身。 您需要了解的是,您正在编写的 lambda 表达式是您找到的抽象方法的实现。 使用 lambda 表达式语法,您可以在代码中很好地内联此实现。
此语法由三个元素组成:
- 参数块;
- 一小块描绘箭头的 ASCII 艺术:
->
. 请注意,Java 使用 微弱的箭头 (->
) 而不是 粗箭头 (=>
); - 一个代码块,它是方法的主体。
让我们看看这方面的例子。 假设你需要一个 Predicate
返回 true
对于恰好有 3 个字符的字符串。
- 你的 lambda 表达式的类型是
Predicate
- 你需要实现的方法是
boolean test(String s)
然后编写参数块,它是方法签名的简单复制/粘贴: (String s)
.
然后添加一个微弱的箭头: ->
.
和方法的主体。 你的结果应该是这样的:
Predicate<String> predicate =
(String s) -> {
return s.length() == 3;
};
简化语法
由于编译器可以猜测很多东西,因此您无需编写它们,因此可以简化此语法。
首先,编译器知道你正在实现的抽象方法 Predicate
接口,它知道这个方法需要一个 String
作为论据。 所以 (String s)
可以简化为 (s)
. 在这种情况下,如果只有一个参数,您甚至可以通过删除括号来更进一步。 参数块然后变成 s
. 如果您有多个参数或没有参数,则应保留括号。
其次,方法体中只有一行代码。 在这种情况下,您不需要花括号,也不需要 return
关键词。
所以最终的语法实际上如下:
Predicate<String> predicate = s -> s.length() == 3;
这将我们引向第一个好的做法:保持 lambda 的简短,以便它们只是一行简单易读的代码。
实施一个 Consumer<String>
在某些时候,人们可能会想走捷径。 您会听到开发人员说“消费者接受一个对象并且什么都不返回”。 或者“当字符串正好有三个字符时,谓词为真”。 大多数情况下,lambda 表达式、它实现的抽象方法和包含此方法的函数接口之间存在混淆。
但是由于函数式接口、它的抽象方法和实现它的 lambda 表达式如此紧密地联系在一起,所以这种说法实际上是完全有道理的。 所以没关系,只要它不会导致任何歧义。
让我们编写一个消耗 String
并打印在 System.out
. 语法可以是这样的:
Consumer<String> print = s -> System.out.println(s);
这里我们直接写了简化版的 lambda 表达式。
实现一个 Runnable
实施一个 Runnable
结果写了一个实现 void run()
. 这个参数块是空的,所以应该用括号写。 记住:只有当你有一个参数时你才能省略括号,这里我们有零。
所以让我们写一个 runnable 告诉我们它正在运行:
Runnable runnable = () -> System.out.println("I am running");
调用 Lambda 表达式
让我们回到之前的 Predicate
例如,假设该谓词已在方法中定义。 如何使用它来测试给定字符串的长度是否确实为 3?
好吧,尽管您使用了用于编写 lambda 的语法,但您需要记住这个 lambda 是接口的一个实例 Predicate
. 这个接口定义了一个方法叫做 test()
这需要一个 String
并返回一个 boolean
.
让我们把它写成一个方法:
List<String> retainStringsOfLength3(List<String> strings) {
Predicate<String> predicate = s -> s.length() == 3;
List<String> stringsOfLength3 = new ArrayList<>();
for (String s: strings) {
if (predicate.test(s)) {
stringsOfLength3.add(s);
}
}
return stringsOfLength3;
}
请注意您如何定义谓词,就像您在前面的示例中所做的那样。 由于 Predicate
接口定义了这个方法 boolean test(String)
,调用中定义的方法是完全合法的 Predicate
通过类型变量 Predicate
. 乍一看这可能令人困惑,因为这个谓词变量看起来不像它定义了方法。
请耐心等待,有更好的方法来编写此代码,您将在本教程后面看到。
因此,每次编写 lambda 时,都可以调用此 lamdba 正在实现的接口上定义的任何方法。 调用抽象方法将调用 lambda 本身的代码,因为此 lambda 是该方法的实现。 调用默认方法将调用接口中编写的代码。 lambda 无法覆盖默认方法。
捕捉本地价值
一旦习惯了它们,编写 lambda 表达式就变得很自然了。 它们很好地集成在 Collections Framework、Stream API 和 JDK 中的许多其他地方。 从 Java SE 8 开始,最好的情况下,lambda 无处不在。
使用 lambda 有一些限制,您可能会遇到需要了解的编译时错误。
让我们考虑以下代码:
int calculateTotalPrice(List<Product> products) {
int totalPrice = 0;
Consumer<Product> consumer =
product -> totalPrice += product.getPrice();
for (Product product: products) {
consumer.accept(product);
}
}
即使此代码看起来不错,尝试编译它也会在使用时出现以下错误 totalPrice
在这 Consumer
执行:
lambda 表达式中使用的变量应该是 final 或有效 final
原因如下:lambda 不能修改在其主体之外定义的变量。 他们可以阅读它们,只要它们是 final
,即不可变。 这个访问变量的过程称为 捕获 :lambdas 不能 捕获 变量,它们只能 捕获 值。 最终变量实际上是一个值。
您已经注意到错误消息告诉我们变量可以是 final ,这是 Java 语言中的一个经典概念。 它还告诉我们变量可以是 有效的 final 。 这个概念是在 Java SE 8 中引入的:即使你没有显式声明一个变量 final
,编译器可能会为您完成。 如果它发现这个变量是从 lambda 读取的,并且你没有修改它,那么它会很好地添加 final
为你声明。 当然这是在编译后的代码中完成的,编译器不会修改你的源代码。 此类变量不称为 final ; 它们被称为 有效的最终 变量。 这是一个非常有用的功能。
具体例子
下面这段代码如果你项目中有用 stream 编程那肯定很熟悉,有一个 Student 的 list,你想把它转换成一个 map,key 是 student 对象的 id,value 就是 student 对象本身。
List<Student> studentList = gen();
Map<String, Student> map = studentList .stream()
.collect(Collectors.toMap(Student::getId, a -> a, (a, b) -> a));
把 Lamda 表达式的部分提取出来。
Collectors.toMap(Student::getId, a -> a, (a, b) -> a)
由于我们还没见过 :: 这种形式,先打回原样,这里只是让你预热一下。
Collectors.toMap(a -> a.getId(), a -> a, (a, b) -> a)
为什么它被写成这个样子呢?我们看下 Collectors.toMap 这个方法的定义就明白了。
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
{
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
看,入参有三个,分别是Function,Function,BinaryOperator,其中 BinaryOperator 只是继承了 BiFunction 并扩展了几个方法,我们没有用到,所以不妨就把它当做BiFunction。
还记得 Function 和 BiFunction 吧?
Function R apply(T t)
BiFunction R apply(T t, U u)
那就很容易理解了。
第一个参数**a -> a.getId()**就是 R apply(T t) 的实现,入参是 Student 类型的对象 a,返回 a.getId()
第二个参数a -> a也是 R apply(T t) 的实现,入参是 Student 类型的 a,返回 a 本身
第三个参数**(a, b) -> a**是 R apply(T t, U u) 的实现,入参是Student 类型的 a 和 b,返回是第一个入参 a,Stream 里把它用作当两个对象 a 和 b 的 key 相同时,value 就取第一个元素 a
其中第二个参数 a -> a 在 Stream 里表示从 list 转为 map 时的 value 值,就用原来的对象自己,你肯定还见过这样的写法。
Collectors.toMap(a -> a.getId(), Function.identity(), (a, b) -> a)
为什么可以这样写,给你看 Function 类的全貌你就明白了。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
...
static <T> Function<T, T> identity() {
return t -> t;
}
}
看到了吧,identity 这个方法,就是帮我们把表达式给实现了,就不用我们自己写了,其实就是包了个方法。这回知道一个函数式接口,为什么有好多还要包含一堆默认方法和静态方法了吧?就是干这个事用的。
我们再来试一个,Predicate 里面有这样一个默认方法。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
}
它能干嘛用呢?我来告诉你,如果没有这个方法,有一段代码你可能会这样写。
Predicate<String> p =
s -> (s != null) &&
!s.isEmpty() &&
s.length() < 5;
如果利用上这个方法,就可以变成如下这种优雅形式。
Predicate<String> nonNull = s -> s != null;
Predicate<String> nonEmpty = s -> s.isEmpty();
Predicate<String> shorterThan5 = s -> s.length() < 5;
Predicate<String> p = nonNull.and(nonEmpty).and(shorterThan5);
自行体会吧。
方法引用
那我们回过头再看刚刚的 Student::getId 这种写法。当方法体中只有一个方法调用时,就可以作这样的简化。
比如这个 a -> a.getId() 就只是对 Student 对象上 getId() 这个方法的调用,那么就可以写成 Student::getId 这种形式。
再看几个例子
Function<String, Integer> toLength = s -> s.length();
Function<String, Integer> toLength = String::length;
Function<User, String> getName = user -> user.getName();
Function<String, Integer> toLength = User::getName;
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer = System.out::println;
如果是构造方法的话,也可以简化。
Supplier<List<String>> newListOfStrings = () -> new ArrayList<>();
Supplier<List<String>> newListOfStrings = ArrayList::new;
总结
学会理解和写 Lambda 表达式,别忘了最开始的三步。
1. 确认 Lamda 表达式的类型
2. 找到要实现的方法
3. 实现这个方法
Lamda 表达式的类型就是函数式接口,要实现的方法就是函数式接口里那个唯一的抽象方法,实现这个方法的方式就是参数块 + 小箭头 + 方法体,其中参数块和方法体都可以一定程度上简化它的写法。
附上参考地址:Writing Your First Lambda Expression
原来 Lambda 表达式是这样写的 (公众号:低并发编程)