Java 8:了解新特性 - Lambdas

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

嗨,亲爱的读者!欢迎到我的博客。在这篇由 3 部分组成的系列文章中的第一篇文章中,我们将讨论 2014 年发布的 Java 8 的新特性。新版本的几个特性改变了我们使用 Java 编写代码时的思维方式。该系列将分为 3 个支柱,每个支柱专门针对一个特定主题,如下所示:

  • 拉姆达;
  • 溪流;
  • java.time(又名新的 Date API);

因此,事不宜迟,让我们先谈谈可能是最著名的新功能:Lambdas!

拉姆达斯

简而言之,Java 上的 lambda 是 Java 生态系统聚合的一种方式,以便能够使用函数式编程。函数式编程范式是一种提倡使用函数的编程范式 - 或者换句话说,带有参数和/或返回值的代码块 - 在调用序列上工作,而不涉及维护变量状态等。使用 lambda,我们可以在我们的代码上创建和存储函数,我们可以在我们的程序中使用这些函数。我们可以采用这种方法的主要好处之一是简化了我们的代码,它比通常的方法更简单。

那么,让我们从示例开始吧!

假设我们想要打印 for 循环中的所有数字,使用一个新线程打印每个数字。在 Java 8 之前的 Java 代码上,我们可以通过编写以下代码来实现:



 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.


上面的代码没有什么问题,除了代码的冗长,因为我们需要声明接口(runnable)和我们想要覆盖的方法(run()),以便创建我们需要创建的内部类线程的实现。如果 Java 语言有任何可以消除这种冗长的特性,那就太好了。现在,有了 Java 8,我们有了这样的特性:lambdas!

让我们重新审视同一个示例,现在使用 lambda:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

正如我们所见,lambda 版本比我们之前的代码简单得多,将线程实现的创建变成了一个简单的单行命令。需要注意的一件有趣的事情是,我们在代码中创建的“可运行”变量不是一个对象,而是一个函数。这意味着 lambda 的“翻译”不同于内部类的解释。当我们打印 lambda 的 getClass 方法的结果时,这一点变得很明显,它将产生如下所示的打印:

我们的 lambda 的“类”:类 com.alexandreesl.handson.LambdaBaseExample$$Lambda$1/424058530

这很有趣,因为如果我们搜索我们项目的编译文件夹,我们可以看到,根据编译器使用的策略,他甚至没有为 lambda 生成 .class 文件,而是内部类!如果读者想更深入地研究 lambda 的解释这个主题, 这个链接 有关于这个主题的更多信息。

读者可能还会注意到,我们不需要将 number 变量声明为 final 以便 lambda 读取值。那是因为在 lambda 的解释中,变量 隐式最终 的概念足以让编译器接受我们的代码。如果我们尝试在代码的任何其他地方使用该变量,我们将收到编译错误。

好吧,一切都很好,但读者可能会质疑: “但是编译器现在如何从 Runnable 接口中尝试覆盖哪个方法呢?”

是为了解决进入 Java 8 上另一个新概念的问题:Functional Interfaces!

函数式接口是一个只有一个抽象方法的接口——默认情况下,接口上的所有方法都是抽象的,除了我们将在稍后讨论的另一个新颖之处——这意味着当编译器检查接口时,他将该方法解释为推断 lambda 的方法。这里的一个关键点是,为了将一个接口提升为一个函数式接口,我们所要做的就是在它上面只有一个抽象方法,所以所有具有这种条件的旧 Java 接口都已经是函数式接口了,比如我们之前使用的 Runnable 接口。如果我们想确保功能接口不会从这种情况下降级,可以使用一个名为 @FunctionalInterface 的新注释。让我们看一个使用这个注解的例子。

让我们创建一个名为 MyInterface 的接口,带有 @FunctionalInterface 注释:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

现在,让我们创建一个类并测试为我们的功能接口创建一个 lambda:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

如果我们运行代码,我们可以看到它按预期工作:

消息是:秘密消息!

现在,让我们尝试向接口添加另一个方法:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

当我们添加此方法并保存时,Eclipse - 如果读者使用 IDE 来编写示例 - 将立即出现编译错误:

描述资源路径位置类型
此表达式的目标类型必须是功能接口 FunctionalInterfaceExample.java /Java8Lambdas/src/main/java/com/alexandreesl/handson 第 7 行 Java 问题

如果我们尝试运行之前创建的类,我们将收到以下错误:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

读者还记得,刚才我们在谈论具有默认抽象方法的接口时谈到了该语言的另一个新颖之处。那么,现在,我们也有可能做不可思议的事情:在接口上实现!所以它进入默认方法!

默认方法

默认方法是接口上的方法,顾名思义,它具有默认实现。让我们在之前的界面上看到这一点。让我们将 MyInterface 更改为以下内容:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

如我们所见,创建默认方法很简单,我们所要做的就是使用关键字 default 并提供实现。为了测试我们的修改,让我们将测试类更改为:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

如果我们运行代码:

消息是:秘密消息!
我收到:秘密消息!
秘密消息!改变了!

可以看到我们的修改成功了。

多重继承

读者可能会问:“天哪!这就是 Java 的多重继承!”。确实,乍一看,情况确实如此,但 Java 8 背后的 Java 开发团队的目标实际上是维护旧的 Java 接口。例如,在 Java 8 中,List 接口具有新方法,例如 forEach 方法,它使我们能够使用 lambda 遍历集合。试想一下,如果我们需要在所有地方实施这种新方法,整个 Java 生态系统(专有框架和开源框架都一样)会多么混乱,更不用说我们自己的 Java 项目代码了!为了防止这种情况,创建了默认方法。

尽管如此,如果读者不相信,规范的领导者已经准备了一个页面来说明他们在这种情况下的论点,例如默认方法不能使用状态变量的事实,因为接口不接受变量。可以 在此处 找到该页面的链接。

方法参考

Java 8 众多的另一个新特性是方法引用。使用方法引用,就像我们使用 lambda 一样,我们可以在访问方法时缩短我们的代码,使代码更具“功能可读性”。让我们以 POJO 为例:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

现在,假设我们想要填充此 POJO 的列表,并通过调用 markClientSpecial 方法对其进行迭代。在 Java 8 之前,我们可以通过执行以下操作来做到这一点:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

我们使用 for 循环进行迭代,显式调用该方法。现在在 Java 8 上,使用 Lambdas,我们可以执行以下操作:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

使用新的 forEach 方法,我们遍历列表的元素,同时调用我们想要的方法。但这还不是全部!使用方法引用,我们还可以执行以下操作:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

使用方法引用语法,我们指明了我们想要执行方法的类——在我们的例子中是 Client 类——以及我们想要执行的方法的引用。 forEach 方法解释我们要对 List 的所有元素执行此方法,正如我们在执行结果中看到的那样:

Java 8 之前的版本!
客户 Alexandre Eleuterio Santos Lourenco 很特别!
客户 Lucebiane Santos Lourenco 很特别!
客户 Ana Carolina Fernandes do Sim 很特别!
带有 LAMBDAS 的 JAVA 8!
客户 Alexandre Eleuterio Santos Lourenco 很特别!
客户 Lucebiane Santos Lourenco 很特别!
客户 Ana Carolina Fernandes do Sim 很特别!
带有方法参考的 JAVA 8!
客户 Alexandre Eleuterio Santos Lourenco 很特别!
客户 Lucebiane Santos Lourenco 很特别!
客户 Ana Carolina Fernandes do Sim 很特别!

方法引用也可以指向引用特定实例的方法。这很有趣,例如,如果我们想创建一个线程,它只会在她的 运行 方法中执行对象实例的方法:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

在我们的示例中,我们仅使用不带参数且不返回值的方法引用,但也可以使用带参数或返回值的方法,例如使用 Consumer Supplier 接口:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

使用方法引用,在某些情况下,我们可以获得比使用 lambda 更简单的代码!

Lambda 的输入

我们将在第一部分讨论的最后一个主题是 lambda 的类型。为了定义 lambda 的类型,编译器使用我们称为 context 的 技术推断类型,这意味着他使用 lambda 所使用的方法或构造函数的上下文来识别 lambda 的类型。例如,如果我们看到第一个 lambda 示例:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

我们可以看到我们将 lambda 声明为 Runnable 类型并传递给了 Thread 类。但是,我们也可以这样编码:


 .

.

.

for (int i = 1; i <= 10; i++) {

final int number = i;

Thread thread = new Thread(new Runnable() {

@Override public void run() {

System.out.println("The number is " + number);

}

});

thread.start();

}

.

.

.

而且代码也可以正常工作。在这种情况下,编译器将利用 Thread 类构造函数的参数类型(一个 Runnable 接口实现)来推断 Lambda 的类型。

结论

这就是我们系列的第一部分。 Java 8 的新特性提出了一种查看我们如何编码的新方法,寻求更简单并支持重构旧接口,这些新特性得以保留,改变了我们开发和发展 Java 项目的方式。感谢您关注这篇文章,直到下一次。

Lambda项目

函数式编程

源代码(Github)