Java中的Lambda表达式
介绍
Lambda 表达式是 Java 8 引入的一项重要特性,它是该语言向 函数式编程 迈出的第一步。这也是为了兼容各种 编程范式 而发展的普遍趋势。
引入 Lambda 表达式的主要动机,是为了减少以往通过匿名内部类模拟函数行为时产生的繁琐样板代码。
请看下面的例子:
String[] arr = { "family", "illegibly", "acquired", "know", "perplexing", "do", "not", "doctors", "where", "handwriting", "I" };
Arrays.sort(arr, new Comparator<String>() {
@Override public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
System.out.println(Arrays.toString(arr));如您所见,实例化一个新的 Comparator 类并覆盖其方法包含了不少重复代码。实际上,这部分逻辑始终是相同的,我们本可以更简洁地表达。
Arrays.sort() 可以使用更简洁的方式替代整个匿名类,且在功能上是等效的:
Arrays.sort(arr, (s1, s2) -> s1.length() - s2.length());这种简短而优雅的代码与冗长的对应代码起到相同的作用,称为 语法糖。这是因为它们没有在语言中添加新功能,而是使代码更加紧凑和易读。Lambda 表达式就是 Java 语法糖的一个典型示例。
尽管建议您按顺序阅读本文,但如果您不熟悉该主题,以下是我们将要涵盖的内容列表,便于参考:
Lambda 表达式与对象
在了解 Lambda 语法本身的本质之前,我们应该先看看什么是 Lambda 函数以及如何使用它们。
如前所述,它们本质上是语法糖,专门针对实现单个方法的接口对象。
在这些对象中,Lambda 实现被视为该方法的实现。如果 Lambda 表达式与接口匹配,则可以将其分配给该接口类型的变量。
函数式接口匹配
为了使 Lambda 表达式与单方法接口(也称为“函数式接口”,Functional Interface)匹配,需要满足几个条件:
- 函数式接口必须仅具有一个未实现的方法,且该方法必须是抽象的。接口中可以包含实现的静态方法和默认方法,但重要的是,只能有一个抽象方法。
- 抽象方法必须以相同的顺序接受与 Lambda 表达式参数相对应的参数。
- 方法和 Lambda 表达式的返回类型必须匹配。
如果满足所有条件,则匹配成功,您可以将 Lambda 表达式分配给变量。
让我们定义一个接口:
public interface HelloWorld {
void world();
}如您所见,这是一个非常简单的函数式接口。
它仅包含一个函数,该函数不接受任何参数且不返回任何值。
我们将使用此接口制作一个简单的 Hello World 程序。如果您想尝试,想象力是无限的:
public class Main {
public static void main(String[] args) {
HelloWorld hello = () -> System.out.println("Hello World!");
hello.world();
}
}如我们所见,运行此代码时,Lambda 表达式已成功匹配 HelloWorld 接口,对象 hello 现在可以用于访问其方法。
其背后的想法是,您可以在任何其他情况下使用 Lambda 来配合函数式接口传递行为。如果您还记得我们的 Comparator 示例,Comparator<T> 实际上就是一个函数式接口,实现了一个方法——compare()。
这就是为什么我们可以用行为类似于该方法的 Lambda 表达式替换它。
实现细节
Lambda 函数背后的基本思想与普通方法相同——它们接收参数输入,并在由表达式组成的主体内使用它们。
只是实现语法有些不同。让我们以 String 排序的 Lambda 为例:
(s1, s2) -> s1.length() - s2.length()其语法可以理解为:
parameters -> body参数
参数与函数参数相同,它们是传递给 Lambda 表达式以供其执行操作的值。
参数通常用括号括起来,并用逗号分隔。尽管在仅接收一个参数的 Lambda 情况下,可以省略括号。
Lambda 表达式可以接受任意数量的参数,包括零,因此您可能会遇到以下情况:
() -> System.out.println("Hello World!")当与相应接口匹配时,此 Lambda 表达式将与以下方法相同:
static void printing() {
System.out.println("Hello World!");
}同样,我们可以使用带有一个、两个或多个参数的 Lambda 表达式。
一个具有一个参数的函数的经典示例是在 forEach 循环中处理集合的每个元素:
public class Main {
public static void main(String[] args) {
LinkedList<Integer> childrenAges = new LinkedList<Integer>(Arrays.asList(2, 4, 5, 7));
childrenAges.forEach(age -> System.out.println("One of the children is " + age + " years old."));
}
}在这里,唯一的参数是 age。请注意,我们在此处删除了括号,因为只有一个参数时允许这样做。
使用更多参数的工作原理类似,它们只是用逗号分隔并括在括号中。当我们将其匹配 Comparator 以对字符串进行排序时,已经看到了两参数 Lambda。
函数体
Lambda 表达式的主体由单个表达式或语句块组成。
如果仅将一个表达式指定为 Lambda 函数的主体(无论是在语句块中还是在其自身中),则 Lambda 将自动返回该表达式的求值结果。
如果语句块中有多行,或者您希望显式声明返回,则可以在语句块中显式使用 return 语句:
// 仅表达式
(s1, s2) -> s1.length() - s2.length()
// 语句块
(s1, s2) -> { return s1.length() - s2.length(); }
// 使用 return (void 返回情况)
(s1, s2) -> {
System.out.println(s1);
return; // 因为某些接口期望 void 返回
}您可以尝试在本文开头将所有这些替换为我们的排序示例,您会发现它们的工作原理完全相同(注意 Comparator 需要返回 int 值)。
变量捕获
变量捕获使 Lambda 可以使用在 Lambda 本身之外声明的变量。
有三种非常相似的变量捕获类型:
- 局部变量捕获
- 实例变量捕获
- 静态变量捕获
语法几乎与您从任何其他函数访问这些变量的方式相同,但是可以使用的条件不同。
仅当局部变量为 effectively final( effectively final,即赋值后不会更改其值)时,您才能访问该局部变量。不必明确将其声明为 final,但建议这样做以避免混淆。如果在 Lambda 函数中使用它,然后尝试更改其值,编译器将报错。
之所以不能这样做,是因为 Lambda 无法可靠地引用可能在其执行前已被销毁或更改的局部变量。因此,它捕获的是变量的值。更改局部变量可能会导致一些令人困惑的行为,因为程序员可能希望 Lambda 中的值会发生变化,因此为避免混淆,明确禁止这样做。
关于实例变量,如果您的 Lambda 与您要访问的变量在同一类之内,则可以简单地使用 this.field 访问该类中的字段。此外,该字段不必是 final,可以在程序执行过程中稍后进行更改。
这是因为,如果在类中定义了 Lambda,则该 Lambda 会与该类一起实例化并绑定到该类实例,因此可以轻松地引用其所需字段的值。
静态变量的捕获与实例变量非常相似,不同之处在于您不会使用 this 来引用它们。出于相同的原因,它们可以更改,并且不必是最终的。
方法引用
有时,Lambda 只是特定方法的替身。本着使语法简短有趣的精神,在这种情况下,您实际上不必键入整个语法。例如:
s -> System.out.println(s)等效于:
System.out::println该 :: 语法将使编译器知道您只需要一个将给定参数传递给 println 的 Lambda。您始终在方法名称前加上 :: 编写 Lambda 函数的位置,否则将像往常一样访问该方法,这意味着您仍然必须在双冒号之前指定所有者类。
方法引用有多种类型,具体取决于您要调用的方法类型:
- 静态方法引用
- 参数方法引用
- 实例方法引用
- 构造方法引用
静态方法引用
我们需要一个接口:
public interface Average {
double average(double a, double b);
}静态函数:
public class LambdaFunctions {
static double averageOfTwo(double a, double b) {
return (a + b) / 2;
}
}然后我们的 Lambda 函数并调用 main:
Average avg = LambdaFunctions::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));参数方法引用
再次,我们输入 main。
Comparator<Double> cmp = Double::compareTo;
Double a = 20.3;
System.out.println(cmp.compare(a, 4.5));该 Double::compareTo Lambda 相当于:
Comparator<Double> cmp = (a, b) -> a.compareTo(b)实例方法引用
如果我们使用 LambdaFunctions 类和函数 averageOfTwo(来自"静态方法引用")并使之成为非静态的,则会得到以下信息:
public class LambdaFunctions {
double averageOfTwo(double a, double b) {
return (a + b) / 2;
}
}要访问它,我们现在需要一个类的实例,因此我们必须在 main 中:
LambdaFunctions lambda = new LambdaFunctions();
Average avg = lambda::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));构造方法引用
如果我们有一个 MyClass 要调用的类并想通过 Lambda 函数调用其构造函数,则我们的 Lambda 将如下所示:
MyClass::new它将接受与构造函数之一匹配的尽可能多的参数。
结论
总之,Lambda 表达式是使我们的代码更简单、更短且更具可读性的有用功能。
当团队中有很多初级人员时,有些人可能会避免使用它们。因此,建议在重构所有代码之前先咨询您的团队。但当每个人都在同一页面上时,它们是一个很好的工具。
参考资料
说明:本文内容基于 Java 8 及以上版本。Lambda 表达式是 Java 8 引入的核心特性,在后续版本中保持兼容。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-zhong-de-lambda-biao-da-shi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。