【底层原理之旅—带你看透Lambda表达式的本质】|Java 刷题打卡

简介: 【底层原理之旅—带你看透Lambda表达式的本质】|Java 刷题打卡

题目


带你看透Lambda表达式的本质




知识点


Lambda的原理


Java 8支持动态语言,看到很酷的Lambda表达式,对一直以静态类型语言自居的Java,让人看到了Java虚拟机可以支持动态语言的目标。



Lambda的案例

import java.util.function.Consumer;
public class Lambda {
  public static void main(String[] args) {
    Consumer<String> c = s -> System.out.println(s);
    c.accept("hello lambda!");
  }
}
复制代码




Lambda表达式


刚看到这个表达式,感觉java的处理方式是属于内部匿名类的方式

public class Lambda {
  static {
    System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
  }
  public static void main(String[] args) {
    Consumer<String> c = new Consumer<String>(){
      @Override
      public void accept(String s) {
        System.out.println(s);
      }
      };
    c.accept("hello lambda");
  }
}
复制代码




编译的结果应该是Lambda.class , Lambda$1.class 猜测在支持动态语言java换汤不换药,在最后编译的时候生成我们常见的方式。 但是结果不是这样的,只是产生了一个Lambda.class

反编译吧,来看看真相是什么?

javap -v -p Lambda.class 
复制代码




  • -p 参数会显示所有的方法,而不带默认是不会反编译private的方法的。
public Lambda();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #21                 // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LLambda;
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: invokedynamic #30,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
         5: astore_1
         6: aload_1
         7: ldc           #31                 // String hello lambda
         9: invokeinterface #33,  2           // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
        14: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
            6       9     1     c   Ljava/util/function/Consumer;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            6       9     1     c   Ljava/util/function/Consumer<Ljava/lang/String;>;
  private static void lambda$0(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #46                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: invokevirtual #50                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0     s   Ljava/lang/String;
 }
SourceFile: "Lambda.java"
BootstrapMethods:
  0: #66 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;
    Method arguments:
      #67 (Ljava/lang/Object;)V
      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
      #71 (Ljava/lang/String;)V
InnerClasses:
     public static final #77= #73 of #75; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
复制代码




重点关注方法


Invokedynamic


  • Java的调用函数的四大指令(invokevirtual、invokespecial、invokestatic、invokeinterface),通常方法的符号引用在静态类型语言编译时就能产生。
  • 动态类型语言只有在运行期才能确定接收者类型,改变四大指令的语意对java的版本有很大的影响,所以在JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》添加了一个新的指令:Invokedynamic。
// InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
0: invokedynamic #30,0
复制代码




  • #30 是代表常量#30也就是后面的注释InvokeDynamic #0:accept: ()Ljava/util/function/Consumer;
  • 0 是占位符号,目前无用




BootstrapMethods


每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site),动态调用点最开始是未链接状态(unlinked):表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。



引导方法是由编译器生成,在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为




CallSite


引导方法的返回值类型是java.lang.invoke.CallSite,一个invokedynamic指令关联一个CallSite,将所有的调用委托到CallSite当前的target(MethodHandle)

InvokeDynamic #0 就是BootstrapMethods表示#0的位置

0: #66 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;
    Method arguments:
      #67 (Ljava/lang/Object;)V
      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
      #71 (Ljava/lang/String;)V
复制代码




我们看到调用了LambdaMetaFactory.metafactory的方法


参数:


  • LambdaMetafactory.metafactory(Lookup, String, MethodType, MethodType, MethodHandle, MethodType) 有六个参数, 按顺序描述如下


1.MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时, JVM会自动自动填充这个参数。



2.String invokedName : 要实现的方法的名字, 使用invokedynamic时, JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name), 在这里JVM为我们填充为 "apply", 即Consumer.accept方法名



3.MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法signature)


  • 使用invokedynamic指令时, JVM会自动自动填充这个参数(填充内容来自常量池InvokeDynamic.NameAndType.Type), 在这里参数为String, 返回值类型为Consumer, 表示这个调用点的目标方法的参数为String, 然后invokedynamic执行完后会返回一个即Consumer实例


4.MethodType samMethodType :  函数对象将要实现的接口方法类型,这里运行时, 值为 (Object)Object 即 Consumer.accept方法的类型(泛型信息被擦除)。 #67 (Ljava/lang/Object;)V


5.MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数)


  • 在这里为 #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V 方法的方法句柄.


6.MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型:



  • 比如函数接口方法定义为 void accept(T t) T为泛型标识, 这个时候方法类型为(Object)Void。


  • 在编译时T已确定, 即T由String替换, 这时samMethodType就是 (Object)Void,
  • instantiatedMethodType为(String)Void



第4,5,6 三个参数来自class文件中的。如上面引导方法字节码中Method arguments后面的三个参数就是将应用于4, 5, 6的参数。




Method arguments:
      #67 (Ljava/lang/Object;)V
      #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
      #71 (Ljava/lang/String;)V
复制代码
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();
}
复制代码




在buildCallSite的函数中

CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();
复制代码




函数spinInnerClass 构建了这个内部类,也就是生成了一个Lambda$$Lambda$1/716157500 这样的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中,如果想看到这个构建的类,可以通过设置环境参数

System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
复制代码




会在你指定的路径 ,当前运行路径上生成这个内部类



静态类


Java在编译表达式的时候会生成lambda$0静态私有类方法,在这个方法里实现了表达式中的方法块 system.out.println(s);

private static void lambda$0(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #46                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: invokevirtual #50                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         7: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0     s   Ljava/lang/String;
复制代码




当然了在上一步通过设置的jdk.internal.lambda.dumpProxyClasses里生成的

Lambda$$Lambda$1.class
 public void accept(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: checkcast     #15                 // class java/lang/String
         4: invokestatic  #21                 // Method Lambda.lambda$0:(Ljava/lang/String;)V
         7: return
    RuntimeVisibleAnnotations:
      0: #13()
复制代码



调用了Lambda.lambda$0静态函数,也就是表达式中的函数块






归纳总结


这样就完成的实现了Lambda表达式,


  1. 使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口
  2. 内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法




目录
打赏
0
0
0
0
379
分享
相关文章
【原理】【Java并发】【synchronized】适合中学者体质的synchronized原理
本文深入解析了Java中`synchronized`关键字的底层原理,从代码块与方法修饰的区别到锁升级机制,内容详尽。通过`monitorenter`和`monitorexit`指令,阐述了`synchronized`实现原子性、有序性和可见性的原理。同时,详细分析了锁升级流程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,结合对象头`MarkWord`的变化,揭示JVM优化锁性能的策略。此外,还探讨了Monitor的内部结构及线程竞争锁的过程,并介绍了锁消除与锁粗化等优化手段。最后,结合实际案例,帮助读者全面理解`synchronized`在并发编程中的作用与细节。
50 8
【原理】【Java并发】【synchronized】适合中学者体质的synchronized原理
|
24天前
|
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
92 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
JVM实战—1.Java代码的运行原理
本文介绍了Java代码的运行机制、JVM类加载机制、JVM内存区域及其作用、垃圾回收机制,并汇总了一些常见问题。
JVM实战—1.Java代码的运行原理
|
20天前
|
怎么理解Java中的lambda表达式
Lambda表达式是JDK8引入的新语法,用于简化匿名内部类的代码写法。其格式为`(参数列表) -&gt; { 方法体 }`,适用于函数式接口(仅含一个抽象方法的接口)。通过Lambda表达式,代码更简洁灵活,提升Java的表达能力。
|
24天前
|
《从头开始学java,一天一个知识点》之:运算符与表达式:算术、比较和逻辑运算
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列为你打造Java「速效救心丸」,每天1分钟,地铁通勤、午休间隙即可完成学习。直击高频考点和实际开发中的「坑位」,拒绝冗长概念,每篇都有可运行的代码示例。明日预告:《控制流程:if-else条件语句实战》。
32 6
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
【JAVA】生成accessToken原理
在Java中,生成accessToken用于身份验证和授权,确保合法用户访问受保护资源。流程包括:1. 身份验证(如用户名密码、OAuth 2.0);2. 生成唯一且安全的令牌;3. 设置令牌有效期并存储;4. 客户端传递令牌,服务器验证其有效性。常见场景为OAuth 2.0协议,涉及客户端注册、用户授权、获取授权码和换取accessToken。示例代码展示了使用Apache HttpClient库模拟OAuth 2.0获取accessToken的过程。
|
3月前
|
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
85 3
|
3月前
|
Java CAS原理和应用场景大揭秘:你掌握了吗?
CAS(Compare and Swap)是一种乐观锁机制,通过硬件指令实现原子操作,确保多线程环境下对共享变量的安全访问。它避免了传统互斥锁的性能开销和线程阻塞问题。CAS操作包含三个步骤:获取期望值、比较当前值与期望值是否相等、若相等则更新为新值。CAS广泛应用于高并发场景,如数据库事务、分布式锁、无锁数据结构等,但需注意ABA问题。Java中常用`java.util.concurrent.atomic`包下的类支持CAS操作。
116 2
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。