Lambda表达式

1.Lambda的字节码

我们先来看看一个Runnable的代码段

接着我把它改造成Lambda表达式,再看一下它的字节码

由此可见它的字节码是invokedynamic。
invokedynamic这个字节码是比较复杂。和反射类似,它用于一些动态的调用场景,但它和反射有着本质的不同,效率也比反射要高得多。
用以下命令反汇编

D:\git\test\target\classes\sandwich\test7> javap -p -v .\LambdaTest.class

可以得到以一串比较奇怪的东西

BootstrapMethods属性在Java 1.7以后才有,位于类文件的属性列表中,这个属性用于保存invokedynamic指令引用的引导方法限定符。 和之前介绍的四个指令(invokestatic,invokespecial,invokevirtual,invokeinterface)不同,invokedynamic并没有确切的接受对象,取而代之的,是一个叫CallSite的对象。
更多详情见JVM规范
https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html#jvms-6.5.invokedynamic

Notes:
If the symbolic reference to the dynamically-computed call site can be resolved, it implies that a non-null reference to an instance of java.lang.invoke.CallSite is bound to the invokedynamic instruction. Therefore, the target method handle, indicated by the instance of java.lang.invoke.CallSite, is non-null.
Similarly, successful resolution implies that the method descriptor in the symbolic reference is semantically equal to the type descriptor of the target method handle.
Together, these invariants mean that an invokedynamic instruction which is bound to an instance of java.lang.invoke.CallSite never throws a NullPointerException or a java.lang.invoke.WrongMethodTypeException.

2.方法句柄(MethodHandle)

invokedynamic指令的底层,是使用方法句柄(MethodHandle)来实现的。方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的get和set方法,从以下案例中可以看到MethodHandle提供的一些方法

MethodHandle中文我们叫它方法句柄。(句柄一般是指获取另一个对象的方法——一个广义的指针,java中没有指针,我更倾向于把它当作一个对象的引用)
更多详情见官方文档
https://docs.oracle.com/javase/7/docs/api/java/lang/invoke/MethodHandles.html
通过这个句柄可以调用相应的方法。
用MethodHandle调用方法的流程如下:

1.创建MethodType,获取指定方法的签名(出参和入参)
2.在Lookup中查找MethodType的方法句柄MethodHandle
3.传入方法参数通过MethodHandle调用方法

2.1 MethodType

MethodType 表示一个方法类型的对象,每个MethodHandle都有一个MethodType 实例,MethodType用来指明方法的返回类型和参数类型。其有多个工厂方法的重载。

MethodType methodType = MethodType.methodType(String.class);

以下是用不同参数创建MethodType :

    public static
    MethodType methodType(Class<?> rtype) {
        return makeImpl(rtype, NO_PTYPES, true);
    }

    public static
    MethodType methodType(Class<?> rtype, Class<?> ptype0) {
        return makeImpl(rtype, new Class<?>[]{ ptype0 }, true);
    }

    public static
    MethodType methodType(Class<?> rtype, MethodType ptypes) {
        return makeImpl(rtype, ptypes.ptypes, true);
    }

    public static
    MethodType methodType(Class<?> rtype, Class<?>[] ptypes) {
        return makeImpl(rtype, ptypes, false);
    }

    public static
    MethodType methodType(Class<?> rtype, List<Class<?>> ptypes) {
        boolean notrust = false;  // random List impl. could return evil ptypes array
        return makeImpl(rtype, listToArray(ptypes), notrust);
    }

    public static
    MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) {
        Class<?>[] ptypes1 = new Class<?>[1+ptypes.length];
        ptypes1[0] = ptype0;
        System.arraycopy(ptypes, 0, ptypes1, 1, ptypes.length);
        return makeImpl(rtype, ptypes1, true);
    }

2.2 Lookup

MethodHandle.Lookup可以通过相应的findxxx方法得到相应的MethodHandle,相当于MethodHandle的工厂方法。查找对象上的工厂方法对应于方法、 构造函数和字段的所有主要用例。
findStatic相当于得到的是一个static方法的句柄(类似于invokestatic的作用),findVirtual找的是普通方法(类似于invokevirtual的作用)

2.3 invoke()

其中需要注意的是invoke和invokeExact,前者在调用的时候可以进行返回值和参数的类型转换工作,而后者是精确匹配的。

package sandwich.test7;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
 * @author 公众号:IT三明治
 * @date 2022/1/10
 */
public class MethodHandlerTest {

    static class Sandwich {
        String whoAreYou() {
            return "I am IT Sandwich";
        }
    }

    static class Jack {
        String whoAreYou() {
            return "I am Jack";
        }
    }

    static class Michael {
        String whoAreYou() {
            return "I am Michael";
        }
    }

    String whoAreYou(Object o) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodType methodType = MethodType.methodType(String.class);
        MethodHandle methodHandle = lookup.findVirtual(o.getClass(), "whoAreYou", methodType);
        return (String) methodHandle.invoke(o);
    }

    public static void main(String[] args) throws Throwable {
        String str = new MethodHandlerTest().whoAreYou(new Sandwich());
        System.out.println(str);
        str = new MethodHandlerTest().whoAreYou(new Jack());
        System.out.println(str);
        str = new MethodHandlerTest().whoAreYou(new Michael());
        System.out.println(str);
    }
}

所以一般在使用是,往往invoke使用比invokeExact要多,因为invokeExact如果类型不匹配,则会抛错。

3. Lambda表达式的捕获与非捕获

当Lambda表达式访问一个定义在 Lambda 表达式体外的非静态变量或者对象时,这个Lambda表达式称为“捕获的”

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

那么“非捕获”的Lambda表达式来就是Lambda表达式没有访问一个定义在Lambda表达式体外的非静态变量或者对象

public static void repeatMsg() {
        Runnable r = () -> {
            System.out.println("Hello Sandwich");
        };
        new Thread(r).start();
    }

Lambda 表达式是否是捕获的和性能悄然相关。一个非捕获的 lambda 通常比捕获的更高效,非捕获的 lambda 只需要计算一次. 然后每次使用到它都会返 回一个唯一的实例。而捕获的 lambda 表达式每次使用时都需要重新计算一次,而且从目前实现来看,它很像实例化一个匿名内部类的实例。 lambda 最差的情况性能内部类一样, 好的情况肯定比内部类性能高。
Oracle 公司的性能比较的文档,详细而全面的比较了lambda表达式和匿名函数之间的性能差别。
lambda 开发组也有一篇PPT其中也讲到了lambda 的性能(包括capture和非capture的情况)。 lambda 最差的情况和性能内部类一样, 好的情况肯定比内部类性能高。
https://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf
http://nerds-central.blogspot.tw/2013/03/java-8-lambdas-they-are-fast-very-fast.html

4.总结

Lambda 语言实际上是通过方法句柄来完成的,大致这么实现(JVM 编译的时候使用invokedynamic实现Lambda表达式,invokedynamic的是使用MethodHandle实现的,所以JVM会根据你编写的Lambda表达式的代码,编译出一套可以去调用MethodHandle的字节码代 码,参考实例类:MethodHandleDemo)
句柄类型(MethodType)是我们对方法的具体描述,配合方法名称,能够定位到一类函数。访问方法句柄和调用原来的指令基本一致,但它的调用异常, 包括一些权限检查,在运行时才能被发现。
可以看到 Lambda 语言实际上是通过方法句柄来完成的,在调用链上自然也多了一些调用步骤,那么在性能上,是否就意味着Lambda性能低呢?对于大部分“非捕获”的 Lambda 表达式来说,JIT编译器的逃逸分析能够优化这部分差异,性能和传统方式无异;但对于“捕获型”的表达式来说,则需要通过方法句柄,不断地生成适配器,性能自然就低了很多(不过和便捷性相比,一丁点性能损失是可接受的)。所以现在的代码检测工具例如SonarLint还是会建议我们用Lambda。
invokedynamic指令,它实际上是通过方法句柄来实现的。和我们关系最大的就是Lambda语法,我们了解原理,可以忽略那些对Lambda性能高低的 争论,同时还是要尽量写一些“非捕获”的Lambda表达式。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT三明治

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值