龙空技术网

Java Lambda表达式详解

so贝塔 3810

前言:

眼前朋友们对“c调用java类”大致比较注重,看官们都需要学习一些“c调用java类”的相关内容。那么小编同时在网摘上收集了一些对于“c调用java类””的相关知识,希望我们能喜欢,咱们一起来了解一下吧!

一、介绍

Java Lambda表达式是Java 8中引入的新特性,它是一种简洁而又强大的闭包语法。Lambda表达式可以替代匿名类,并且更加易读和简洁。以下是有关Java Lambda表达式的详细信息:

Lambda表达式的语法

Lambda表达式的语法如下:

(parameters) -> expression

或者

(parameters) -> { statements; }

其中,parameters表示Lambda表达式的参数列表,可以是空的或包含一个或多个参数。箭头符号->用于分隔参数列表和Lambda表达式的主体。如果Lambda表达式主体只有一条语句,则可以使用第一种语法,否则需要使用第二种语法,并使用大括号括起来。

Lambda表达式的类型

Lambda表达式本质上是一个函数,因此它具有类型。Lambda表达式的类型由两部分组成:参数类型和返回类型。这些类型可以根据Lambda表达式主体中的代码自动推断,因此通常不需要显式指定。

Lambda表达式的应用

Lambda表达式可以用在Java中支持函数式接口的任何场景中,例如:

集合框架中的过滤器和映射器线程和并发编程中的回调GUI编程中的事件处理程序Web应用程序中的路由处理程序等

Lambda表达式的优点

Lambda表达式相对于匿名类具有以下优点:

更加简洁:Lambda表达式通常比匿名类更短,更易读。更加直观:Lambda表达式可以使代码更加直观,易于理解。更加高效:Lambda表达式不需要创建新的类或实例,因此在某些情况下可以提高性能。

综上所述,Java Lambda表达式是一种非常强大和实用的语言特性,它可以帮助我们更轻松地编写函数式代码。

二、例子

以下是一个使用Lambda表达式的实际例子,该例子使用Lambda表达式过滤列表中的元素:

import java.util.ArrayList;import java.util.List;import java.util.stream.Collectors;public class Main {    public static void main(String[] args) {        List<String> names = new ArrayList<>();        names.add("Alice");        names.add("Bob");        names.add("Charlie");        names.add("Dave");        names.add("Eve");        // 使用Lambda表达式过滤列表中的元素        List<String> filteredNames = names.stream()                .filter(name -> name.length() > 4)                .collect(Collectors.toList());        System.out.println(filteredNames);    }}

在上述代码中,我们使用Lambda表达式来过滤列表中长度大于4的字符串。我们使用Java8 Stream API中的filter()方法,该方法接受一个函数式接口Predicate作为参数,并返回一个Stream对象。在这里,我们使用Lambda表达式作为Predicate的实现,并检查每个字符串的长度是否大于4。

然后,我们使用collect()方法将Stream对象转换为List对象,并打印过滤后的结果。这个例子演示了Lambda表达式的实际应用,它使得代码更加简洁、直观、易懂。

三、原理

经过上面的介绍,我们看到 Lambda 表达式只是为了简化匿名内部类书写,看起来似乎在编译阶段把所有的 Lambda 表达式替换成匿名内部类就可以了。但实际情况并非如此,在 JVM 层面,Lambda 表达式和匿名内部类其实有着明显的差别。

匿名内部类的实现

匿名内部类仍然是一个类,只是不需要我们显式指定类名,编译器会自动为该类取名。比如有如下形式的代码:

public classLamTest {    publicstaticvoidmain(String[] args) {        newThread(newRunnable() {           @Overridepublicvoidrun() {                System.out.println("lambda test");           }       }).start();   }}

编译之后将会产生两个 class 文件:LamTest.class LamTest$1.class

使用 javap -c LamTest.class 进一步分析 LamTest.class 的字节码,部分结果如下:

可以发现在 4: new #3 这一行创建了匿名内部类的对象。

Lambda 表达式的实现

接下来我们将上面的示例代码使用 Lambda 表达式实现,代码如下:

public classLamTest {    public static voidmain(String[] args) {        newThread(()->System.out.println("lambda test")).start();   }}

此时编译后只会产生一个文件 LamTest.class,再来看看通过 javap 对该文件反编译后的结果。

从上面的结果我们发现 Lambda 表达式被封装成了主类的一个私有方法,并通过 invokedynamic 指令进行调用。

其实在设计 Lambda 表达式的解析方案时,主要考虑下面两点:

可扩展性,不把解析方案写死在字节码上;

解析 Lambda 表达式后,对字节码文件干扰尽量降到最低,起码保证一定的可读性。

正式基于以上两点考虑, invokedynamic 指令被运用到解析 Lambda 表达式,优势如下:

字节码表示简单,利用 invokedynamic 指令本身的特性(引导方法),把 Lambda 表达式在字节码文件的表示和真正的解析分离;

Lambda 表达式的链接解析发生运行期而非编译期,由 invokedynamic 的引导方法执行,扩展性强。

Lambda 是面向方法接口编程,其实我更多认为是 JDK8 提供的语法糖,因为对非方法引用的 Lambda 表达式,编译器都会为其生成一个方法实现 Lambda 表达式的逻辑,并出现在编译后的字节码文件中。这个方法的生成是 Lambda 表达式解析的关键,Lambda 表达式的解析分成三个阶段:

链接阶段, 负责生成动态调用点变量捕获,闭包中的变量访问Lambda 表达式调用

链接阶段由 invokedynamic 指令后面跟随的 Bootstrap Method 引导方法完成,生成动态调用点。我们先来看 Lambda 表达式解析用的最多的一个引导方法:

java.lang.invoke.LambdaMetafactory#metaFactory:

该启动方法以及参数都是编译器指定,下面来解释下这几个参数的含义:

caller 指的是 MethodHandle,lookupClass 指向 enclosing object

invokedName 需要实现的函数式接口方法名

invokedType 变量捕获列表

samMethodType 需要实现的函数式接口方法签名(sam 是 single abstract method )

implMethod 指向生成的 desugaring method

instantiatedMethodType 对应运行时 sam 的方法签名,运行时签名验证,在非泛型情况下,和 samMethodType 相同;比如下面代码中,samMethodType 是(Ljava/lang/Object)V, instantiatedMethodType 则是(Ljava/lang/String)V

引导方法 metaMethod 根据这些参数生成 java.lang.invoke.CallSite 动态调用点对象支持 Lambda 表达式的调用执行。在引导方法中会动态生成一个模板匿名类,查看这个类的字节码可以通过加上 -Djdk.internal.lambda.dumpProxyClasses 参数指定 dump 的目录,该匿名是一个模板类,其特点如下:

实现函数式接口,内部逻辑很简单,调用上面提到的脱糖方法(desugaring method);

生成一个构造方法, 构造方法参数为被捕获参数变量,所有变量存储为类实例变量;

如果被捕获参数变量列表不为空, 则会生成一个工厂方法 get$lambad,方法签名和构造方法相同;该工厂方法的作用是避免重复调用 asm 生成匿名类字节码,提升性能;在下一节的性能分析中会提到这个方法的作用。

因此,我们可以得出结论:Lambda 表达式是通过 invokedynamic 指令实现的,并且书写 Lambda 表达式不会产生新的类。

标签: #c调用java类