13

Lambda表达式介绍和底层实现分析

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzUxMTcwOTM4Mg%3D%3D&%3Bmid=2247488214&%3Bidx=1&%3Bsn=cb07f3a8785667baa2fce94697ca6312
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

奇技 ·  指南

今天小编为大家分享一篇关于SSH 的介绍和使用方法的文章。本文从SSH是什么出发,讲述了SSH的基本用法,之后在远程登录、端口转发等多种场景下进行独立的讲述,希望能对大家有所帮助。

lambda表达式

lambda表达式,也被称为闭包,它是推动 Java 8 发布的最重要新特性。lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

lambda 表达式的语法格式如下:

(parameters) -> expression或 (parameters) ->{ statements; }

以下是lambda表达式的重要特征:

可选类型声明

不需要声明参数类型,编译器可以统一识别参数值;

可选的参数圆括号

一个参数无需定义圆括号,但多个参数需要定义圆括号;

可选的大括号

如果主体包含了一个语句,就不需要使用大括号;

可选的返回关键字

如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

例如下面是一些lambda表达式

(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }

第一个 lambda 表达式接收 x 和 y 这两个整形参数并返回它们的和; 第二个 lambda表达式不接收参数,返回整数 ‘42’; 第三个 lambda 表达式接收一个字符串并把它打印到控制台,不返回值。

了解使用lambda表达式,需要了解函数式接口,lambda用来代替内部类,赋予了Java简单但是强大的函数式编程能力,同时可以认为java支持命令式编程、声明式编程、函数式编程。

函数式接口

只有一个抽象方法的接口,称为函数式接口。例如下面的接口,

public interface MyFunctionInterface<T> {
public T getValue(T t);
}

测试类如下

public class MyFunctionInterfaceTest {
public static void main(String[] args) {
testMethod(" aaaa ", s -> s.trim());
testMethod(" aaaa ", s -> s.trim().toUpperCase());
}

public static void testMethod(String str, MyFunctionInterface<String> functionInterface) {
System.out.println(functionInterface.getValue(str));
}
}

输出结果如下:

aaaa
AAAA

修改一下上面的接口,增加一个方法。

public interface MyFunctionInterface<T> {
public T getValue(T t);
public T returnValue(T t);
}

增加一个方法之后,上面就不是函数式接口了,可以看到lambda表达式就会报错。

JfmmMzB.png!mobile

虽然不能在函数式接口中定义多个方法,但可以定义默认方法、静态方法以及java.lang.Object里的public方法。如下面的代码

@FunctionalInterface
public interface MyFunctionInterface<T> {

public T getValue(T t);

default void defaultMethod() {
System.out.println("this is default method");
}

static void staticMethod() {
System.out.println("this is static method");
}


public boolean equals(Object obj);
}

我们可以在接口上使用@FunctionalInterface注解,如果使用Intellij IDEA可以在编码的时候就看见报错了,这样做可以检查它是否是一个函数式接口。

3eQr6nm.jpg!mobile

通过反编译,可以看到函数式接口其实就是一个普通的java接口类,如下图

aAVvme7.png!mobile

函数式接口可以作为方法参数传递lambda表达式,但是为了将Lambda表达式作为参数传递,接收Lambda表达式的参数类型必须是与该Lambda表达式兼容的函数式接口的类型。但是我们没必要为每一个lambda表达式创建接口,在jdk的java.util.function包下面已经为我们创建了常用的函数式接口,其中比较核心的是消费型接口(Consumer<T>),供给型接口(Supplier<T>),断言型接口(Predicate<T>),函数型接口(Function<T, R>)四个接口,能够满足大部分应用场景。

lambda表达式原理分析

继续使用上面的测试代码,可以在IDEA中使用
jclasslib Bytecode viewer 
插件查看MyFunctionInterface.class源文件。

安装完jclasslib Bytecode viewer,会在view菜单中出现如下两个选项

F3INJb2.png!mobile

选择需要反编译的class文件,点击Show Bytecode with Jclasslib选项会出现如下界面

v6rqQ3E.png!mobile

可以看到里面编译器多生成了lambda$main$0和lambda$main$1两个私有静态方法,属性当中多了InnerClasses。我们可以通过Show Bytecode查看一下测试类字节码更详细的反编译结果,找到这两个静态方法。

// access flags 0x100A
private static synthetic lambda$main$1(Ljava/lang/String;)Ljava/lang/String;
L0
LINENUMBER 6 L0
ALOAD 0
INVOKEVIRTUAL java/lang/String.trim ()Ljava/lang/String;
INVOKEVIRTUAL java/lang/String.toUpperCase ()Ljava/lang/String;
ARETURN
L1
LOCALVARIABLE s Ljava/lang/String; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x100A
private static synthetic lambda$main$0(Ljava/lang/String;)Ljava/lang/String;
L0
LINENUMBER 5 L0
ALOAD 0
INVOKEVIRTUAL java/lang/String.trim ()Ljava/lang/String;
ARETURN
L1
LOCALVARIABLE s Ljava/lang/String; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}

可以看到两个私有的静态方法干的就是Lambda表达式里面的内容,那么又是如何调用的生成的私有静态方法呢? 如下图,通过分析main方法的L0,首先通过INVOKEDYNAMIC 指令调用是MyFunctionInterface的getValue方法的引用,以及后面的BootstrapMethods #0。 使用jclasslib Bytecode viewer查看。

2uEf22b.png!mobile

点击#3,进入下面界面

点击BootstrapMethods #0,进入如下界面

ryQJZjn.png!mobile

点击cp_info #44,进入如下界面

2mAZfqe.png!mobile

继续点击相应的方法描述符,我们可以看到最后

cp_info #74 内容如下:

(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;

可以看到INVOKEDYNAMIC 后面的一系列指令,最后使用INVOKESTATIC调用

java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;

查看LambdaMetafactory.metafactory的方法,

里面通过InnerClassLambdaMetafactory生成了CallSite的子类ConstantCallSite,当通过指令调用CallSite会返回函数式接口的实例,而生成接口实例的方式是通过内部类的方式,由于方法比较深,就不继续贴代码了。

public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)

throws LambdaConversionException
{
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}

fYjyQjj.png!mobile

CallSite持有
com/para/lambda/MyFunctionInterfaceTest.lambda$main$0方法的句柄,这个句柄会调用该方法。

JBJja2u.png!mobile

所以使用lambda表达式的地方,会在类编译的时候在本类中生成对应的私有静态方法和一个INNERCLASS的访问标识(具体是什么东西没找到资料,注释显示是一个访问标识),该访问标识会调用引导类加载器动态生成内部类,该内部类实现了函数式接口,在实现接口的方法中,会调用编译器生成静态方法,在使用lambda表达式的地方,通过传递内部类实例,来调用函数式接口方法。

本文从lambda表达式、函数式接口的介绍和对lambda表达式底层原理的分析来认识java中的函数式编程。函数式接口本身就是一个普通的接口,而lambda表达式本质上和匿名内部类是一样的,只不过条件更加苛刻。使用lamda表达式可以以一种更优雅的方式来编程。

往期精彩回顾

奇麟|一站式大数据资源管理与开发平台

rIfuyy.png!mobile
b6bMBjv.png!mobile

360技术公众号

技术干货|一手资讯|精彩活动

扫码关注我们


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK