Peter-Java 8中的Lambda表达式

在java8以前,想要传递代码块很不容易。我们只能把代码块写在一个特殊类里,然后实例化一个类对象来传递这段代码。

在其他语言中,例如C#,则可以直接使用代码块。java语言设计者多年来一直反对添加此功能。理由无非是想要保持语法的简单性和一致性。但却牺牲了编码便利性。

在下一节中,我们一起来了解如何在Java中使用代码块。

Lambdas的语法

让我们再次回到字符串排序。我们提供了确定哪个字符串更短的代码。我们计算

Integer.compare(firstStr.length(), secondStr.length())

这一行代码无非表达了一个意思,使用Integer.compare对firstStrsecondStr进入排序。

让我们用提问的方式来更明确的描述这个意思:

1、我们要处理的入参数数据是什么?是什么数据类型?

2、使用什么代码片断来对它们进行处理?

有了提问,回答就容易了。是对这样的入参数据进行处理(String firstStr, String secondStr),使用这样的 Integer.compare(firstStr.length(),secondStr.length()) 代码片断。

于是,有了我们第一个lambda表达式!此表达式指定代码块和必须传递给代码块的变量。

(String firstStr, String secondStr)
-> Integer.compare(firstStr.length(),secondStr.length())

还有一点历史…关于lambda这个名字的来历?很久以前,在计算机还没有出世的时候,数学家Alonzo Church想要形式化数学函数有效计算的意义。(有一些已知存在的函数,但没有人知道如何计算它们的值。)他使用希腊符号lambda(λ)来标记参数。从那以后,带有参数变量的表达式被称为“lambda表达式”。

Java lambda略有几种不同的形式。让我们更仔细地考虑一下。您刚刚看到其中一个:参数, - >箭头和表达式。如果代码包含的计算不适合单个表达式,那么就像编写方法一样编写它:将代码放入{}并添加显式return语句。例如,

(String firstStr, String secondStr) -> {
if (firstStr.length() < secondStr.length()) return -1;
else if (firstStr.length() > secondStr.length()) return 1;
else return 0;
}

如果lambda中没有参数,你仍然应该放置空括号,就像无参数方法一样:

() -> { for (int i = 0; i < 1000; i++) doSomething(); }

如果可以推断lambda的参数类型,则可以省略它们。例如,

Comparator comp
= (firstStr, secondStr) // Same as (String firstStr, String secondStr)
-> Integer.compare(firstStr.length(),secondStr.length());

此时,编译器可以找出firstStr并且secondStr是字符串,因为我们将lambda分配给字符串比较器。(我们稍后会仔细研究这段代码。)

如果一个方法只有一个参数,编译器可以推导出是哪种类型,你甚至可以省略括号:

EventHandler listener = event ->
System.out.println(“The button has been clicked!”);
// Instead of (event) -> or (ActionEvent event) ->

此外,您可以像final方法参数一样,将修饰符和注释放在lambda参数中:

(final String var) -> …
(@NonNull String var) -> …

您永远不需要指定lambda表达式的结果类型。编译器总是从上下文中推断出它。例如,您可以使用lambda

(String firstStr, String secondStr) -> Integer.compare(firstStr.length(), secondStr.length())

其中int预期作为结果类型。

请注意,在lambda中,您不能返回不在分支中的值。例如,(int x) -> { if (x <= 1) return -1; }无效。

功能接口

像我们文章开头讨论的那样,Java可以借用接口来封装代码块,比如RunnableComparator。这对Lambdas同样适用。

在Java中有所谓的功能接口 - 一个只有单个抽象方法实现的接口对象。只要需要功能接口的对象,就可以使用lambda表达式。

让我们考虑一下Arrays.sort方法的例子。在这里我们可以看到用lambda替换功能接口。我们只是将lambda作为第二个参数传递给方法,该参数需要一个Comparator对象,该接口只有一个方法。

Arrays.sort(strs,
(firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length()));

实际上该Arrays.sort方法接收一些类实现的对象Comparator<String>compare调用该方法时,它会强制执行lambda表达式主体。这些对象和类的结构完全取决于实现。它不仅可以使用传统的内部类。也许最好将lambda表示为一个函数,而不是作为一个对象,并发现我们可以将它传递给一个功能接口。

这种对接口的转换是lambda表达式令人兴奋的原因。语法简短。这是另一个例子:

button.setOnAction(event ->
System.out.println(“The button has been clicked!”));

是不是很易读?

事实上,你在Java中使用lambda表达式唯一能做的就是转换。

Java API中的java.util.function包中有几个通用的功能接口。其中之一,BiFunction<T, U, R>代表与参数类型的函数TU和返回类型R。您可以将字符串比较lambda传给这样的变量:

BiFunction<String, String, Integer> compareFunc
= (firstStr, secondStr) -> Integer.compare(firstStr.length(), secondStr.length());

您可以在不同的Java 8 API中看到java.util.function中的这些接口。在Java 8中,任何功能接口都可以用@FunctionalInterface。这个注释是可选的,但却是一个很好的风格。首先,它强制编译器检查带注释的实体是否是具有单个抽象方法的接口。第二是告诉javadoc页面包含一个声明,这个接口是一个功能接口。根据定义,任何只有一个抽象方法的接口都是一个功能接口。但是,使用此关键字可以更加清晰。

顺便说一句,在将lambda转换为功能接口时,可能会出现已检查的异常。如果lambda表达式的主体抛出已检查的异常,则应在目标接口的抽象方法中声明此异常。例如,以下代码将导致错误:

Runnable sleepingRunner = () -> { System.out.println(“…”); Thread.sleep(1000); };
// Error: Thread.sleep can throw a checkedInterruptedException

此语句不正确,因为该run方法不能抛出任何异常。有两种方法应对此问题。

一种方法是捕获lambda体中的异常。第二个是将此lambda分配给具有单个抽象方法的接口,该方法可以抛出异常。例如,call接口的方法Callable可以生成任何异常。因此,如果return null在lambda主体的末尾添加,则可以将lambda分配给Callable<Void>实例。

方法参考

有时你已经有了一个适合你需求的方法,你想将它传递给其他一些方案。例如,假设您希望在单击按钮时只打印事件对象。你可以这样写

button.setOnAction(event -> System.out.println(event));

println方法传递给setOnAction方法更直观。以下示例显示了它:

button.setOnAction(System.out::println);

System.out::println是一个方法引用,类似于lambda表达式。我们可以在这里用方法引用替换lambda。

想象一下,你想要忽略一个字母大小写的排序字符串。您可以编写如下代码:

Arrays.sort(strs, String::compareToIgnoreCase)

运算符::将方法名称与对象或类的名称分开。主要有三种情况:

  • 对象的实例方法;
  • 一个类的静态方法;
  • 类的实例方法;

在前两种情况下,方法引用等效于带有方法参数的lambda表达式。如上所示,System.out::println相当于x -> System.out.println(x)。同样,Math::pow相当于(x, y) -> Math.pow(x, y)。在第三种情况下,第一个参数成为方法的目标。例如,String::compareToIgnoreCase与…相同(x, y) -> x.compareToIgnoreCase(y)

众所周知,类可以有多个具有相同名称的重载方法。在这种情况下,编译器将尝试从上下文中找到要选择的内容。例如,该Math.max方法有两个版本,一个用于int,一个用于double值。调用哪一个取决于Math::max转换的功能接口的方法签名。方法引用不是单独存在的。与幕后的lambdas类似,它们总是变成功能接口的实例。

您可能想到是否可以使用this在方法参考中捕获参数。是的你可以。例如,this::equals相当于x -> this.equals(x)。它也可以使用super。当我们使用super::instanceMethod它成为目标并调用给定方法的基类版本。这是一个非常真实的例子:

class Speaker {
public void speak() {
System.out.println(“Hello, world!”);
}
}
class ConcurrentSpeaker extends Speaker {
public void speak() {
Thread t = new Thread(super::speak);
t.start();
}
}

当线程启动时,run调用方法并super::speak执行,调用speak其基类的方法。请注意,在内部类中,您可以this将封闭类的引用,捕获为EnclosingClass.this::methodEnclosingClass.super::method

构造函数参考

构造函数引用与方法引用相同,只是方法名称为new。例如,Button::new是类的构造函数引用Button。将调用哪个构造函数取决于上下文。想象一下,您想要将字符串列表转换为按钮数组。在这种情况下,您应该在每个字符串上调用构造函数。它可能是这样的:

List strs = …;
Stream stream = strs.stream().map(Button::new);
List buttons = stream.collect(Collectors.toList());

有关stream的更多信息,您可以查看文档。在这种情况下,最重要的是该方法为每个列表元素调用构造函数。有多个构造函数,但编译器选择带有参数的构造函数,因为从上下文中可以明显看出应该调用带有字符串的构造函数。map``collect``map``Button(String)``Button``String

还可以为数组类型创建构造函数引用。例如,int数组的构造函数引用是int[]::new。它需要一个参数:数组的长度。它等同于lambda表达式x -> new int[x]

数组的构造函数引用对于超越Java的限制很有用。创建泛型类型的数组是不可能的T。表达式new T[n]不正确,因为它将替换为new Object[n]。对于图书馆作者来说这是一个问题。想象一下,我们想拥有一系列按钮。有一种方法toArray在类Stream返回的数组Object

Object[] buttons = stream.toArray();

但那不是你想要的。用户不想要Objects,只有按钮。该库使用构造函数引用解决了这个问题。你应该传递Button[]::new给方法toArray

Button[] buttons = stream.toArray(Button[]::new);

toArray方法调用此构造函数以获取所需类型的数组,然后在填充后返回此数组。

可变范围

通常,您希望从lambda表达式中的封闭范围访问变量。考虑以下代码:

public static void repeatText(String text, int count) {
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
Thread.yield();
}
};
new Thread®.start();
}

看下面这个调用:

repeatText(“Hi!”, 2000); // Prints Hi 2000 times in a separate thread

注意变量counttext没有在lambda表达式中定义; 这些是封闭方法的参数。

如果仔细观察这段代码,你会发现幕后有某种魔力。该repeatText方法可以在lambda表达式的代码运行之前返回,而那时参数counttext变量将消失,但它们仍然可用于lambda。秘密是什么?

要了解发生了什么,我们需要提高对lambda表达式的理解。lambda表达式由三个部分组成:

  1. 代码块
  2. 参数
  3. 自由变量不是参数,也没有在lambda中定义

在我们的案例中有两个自由变量,textcount。表示lambda的数据结构必须存储它们的值,“嗨!” 他们说这些值是由lambda表达式捕获的。(如何完成取决于实现。例如,实现可以使用一个方法将lambda表达式转换为对象,并将自由变量的值复制到对象的实例变量中。)

有一个特殊的术语“封闭”; 它是一个代码块和自由变量的值。Lambda用一种方便的语法表示Java中的闭包。顺便说一句,内部类总是封闭的。

因此,lambda表达式可以捕获封闭范围中变量的值,只是有一些限制。你无法更改这些捕获变量的值。以下代码不正确:

public static void repeatText(String text, int count) {
Runnable r = () -> {
while (count > 0) {
count–; // Error: Can’t modify captured variable
System.out.println(text);
Thread.yield();
}
};
new Thread®.start();
}

这种限制是合理的,因为lambda表达式中的变量变量不是线程安全的。想象一下,我们有一系列并发任务,每个任务都更新一个共享计数器。

int matchCount = 0;
for (Path p : files)
new Thread(() -> { if (p has some property) matchCount++; }).start();
// Illegal to change matchCount

如果这段代码是正确的,那将是非常非常糟糕的。increment运算符++不是原子的,如果多个线程同时执行此代码,则无法控制结果。

内部类也可以从外部类中捕获值。在Java 8之前,内部类只能访问final局部变量。此规则已扩展为与lambda表达式匹配。现在,内部类可以处理其值不会发生变化的任何变量(实际上是final变量)。

不要指望编译器捕获所有并发访问错误。您应该知道,此修改规则仅适用于局部变量。如果我们使用外部类的实例或静态变量,则不会出现错误,结果是未定义的。

您也可以修改共享对象,即使它不健全。例如,

List matchedObjs = new ArrayList<>();
for (Path p : files)
new Thread(() -> { if (p has some property) matchedObjs.add§; }).start();
// Legal to change matchedObjs, but unsafe

如果你仔细观察,你会发现变量matchedObjs实际上是final的。(有效的final变量是一个在初始化后再无变化的变量。)在此代码中,matchedObjs始终引用同一个对象。但是,该变量matchedObjs已被修改,并且它不是线程安全的。在多线程环境中运行此代码的结果是不可预测的。

这种多线程任务有安全的机制。例如,您可以使用线程安全计数器和集合,流来收集值。

对于内部类,有一种解决方法允许lambda表达式更新封闭的本地范围中的计数器。想象一下,你使用长度为1的数组,如下所示:

int[] counts = new int[1];
button.setOnAction(event -> counts[0]++);

很明显,这段代码不是线程安全的。对于按钮回调,这没关系,但一般来说,在使用这个技巧之前你应该三思而后行。

lambda的主体与嵌套块具有相同的范围。名称冲突和阴影的规则是相同的。您不能在lambda中声明与封闭范围中的变量同名的参数或局部变量。

Path first = Paths.get(“/usr/local”);
Comparator comp =
(first, second) -> Integer.compare(first.length(), second.length());
// Error: Variable first already defined

在方法中不能有两个具有相同名称的局部变量。因此,您也不能在lambda表达式中引入这些变量。同样的规则适用于lambda。在thislambda中使用关键字时,请参考this创建lambda的方法。我们考虑一下这段代码

public class Application() {
public void doSomething() {
Runnable r = () -> { …; System.out.println(this.toString()); … };

}
}

在此示例中<code>this.toString()调用对象的toString方法Application,而不是Runnable实例。this在lambda表达式中使用没有什么特别之处。lambda表达式的范围嵌套在doSomething方法中,并且在该方法中的任何位置都具有相同的含义。

默认方法

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
1715866849509)]

[外链图片转存中…(img-HzYCeVte-1715866849509)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值