Java 8 动态类型语言Lambda表达式实现原理分析

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

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!");
	}
}

刚看到这个表达式,感觉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 这个参数 -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

在这里我们发现了几个与我们常见的java不太一样的地方,由于常量定义太多了,文章中就不贴出了

1. Invokedynamic 指令

Java的调用函数的四大指令(invokevirtual、invokespecial、invokestatic、invokeinterface),通常方法的符号引用在静态类型语言编译时就能产生,而动态类型语言只有在运行期才能确定接收者类型,改变四大指令的语意对java的版本有很大的影响,所以在JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》添加了一个新的指令

Invokedynamic

0: invokedynamic #30,  0             // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

#30 是代表常量#30 也就是后面的注释InvokeDynamic #0:accept:()Ljava/util/function/Consumer;

0 是占位符号,目前无用

2. BootstrapMethods

每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site), 动态调用点最开始是未链接状态(unlinked:表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法.  引导方法是由编译器生成, 在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为.引导方法的返回值类型是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

我们来看metafactory 的方法里的实现代码

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", ".");

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

3.静态类

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表达式,使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口,内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法

时间: 2024-09-30 12:46:06

Java 8 动态类型语言Lambda表达式实现原理分析的相关文章

Java Lambda表达式 实现原理分析

https://blog.csdn.net/qq_37960603/article/details/85028867 在类编译时,会生成一个私有静态方法+一个内部类. 在内部类中实现了函数式接口,在实现接口的方法中,会调用编译器生成的静态方法. 在使用lambda表达式的地方,通过传递内部类实例,来调用函数式接口方法. 原文地址:https://www.cnblogs.com/eryun/p/12149841.html

【转】解析JDK 7的动态类型语言支持

http://www.infoq.com/cn/articles/jdk-dynamically-typed-language Java虚拟机的字节码指令集的数量自从Sun公司的第一款Java虚拟机问世至JDK 7来临之前的十余年时间里,一直没有发生任何变化[1].随着JDK 7的发布,字节码指令集终于迎来了第一位新成员——invokedynamic指令.这条新增加的指令是JDK 7实现“动态类型语言(Dynamically Typed Language)”支持而进行的改进之一,也是为JDK 8

Java编程的逻辑 (91) - Lambda表达式

在之前的章节中,我们的讨论基本都是基于Java 7的,从本节开始,我们探讨Java 8的一些特性,主要内容包括: 传递行为代码 - Lambda表达式 函数式数据处理 - 流 组合式异步编程 - CompletableFuture 新的日期和时间API 本节,我们先讨论Lambda表达式,它是什么?有什么用呢? Lambda表达式是Java 8新引入的一种语法,是一种紧凑的传递代码的方式,它的名字来源于学术界的λ演算,具体我们就不探讨了. 理解Lambda表达式,我们先回顾一下接口.匿名内部类和

编译型语言、解释型语言、静态类型语言、动态类型语言概念与区别

最近在研究Python和Erlang.反复提到动态类型语言.动态语言.解释型语言这些概念.这些概念很生涩,在这里做一个总结. 编译型语言和解释型语言 1.编译型语言 需通过编译器(compiler)将源代码编译成机器码,之后才能执行的语言.一般需经过编译(compile).链接(linker)这两个步骤.编译是把源代码编译成机器码,链接是把各个模块的机器码和依赖库串连起来生成可执行文件. 优点:编译器一般会有预编译的过程对代码进行优化.因为编译只做一次,运行时不需要编译,所以编译型语言的程序执行

动态语言、动态类型语言、静态类型语言、强类型语言、弱类型语言

关于如题这几个概念,大部分人应该比较熟悉,但是我昏了好久,所以写下来加深印象. 1. 动态语言 又叫动态编程语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化.比如众所周知的ECMAScript(JavaScript)便是一个动态语言.除此之外如Ruby.Python等也都属于动态语言,而C.C++等语言则不属于动态语言. 而其实我们平时说的"动态语言"应该指的是"动态类型语言".至少我之前一直是这么认为的. 2. 动态类

动态语言/动态类型语言/静态类型语言

动态语言 指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除,即结构上的变化 如众所周知的ECMAScrip(JavaScript)便是一个动态语言 除此之外,Ruby.Python也都属于动态语言 C与C++则不属于动态语言 动态类型语言 指类型检查是在运行时做的 静态类型语言 分析 类型检查是在运行前判断(如编译阶段),如C#和java都是静态类型语言 静态类型语言为了达到多态,会采取一些类型鉴别手段,如继承.接口 而动态类型语言不需要,所以一般动态语言都会采用dynam

动态生成C# Lambda表达式

转载:http://www.educity.cn/develop/1407905.html,并整理! 对于C# Lambda的理解我们在之前的文章中已经讲述过了,那么作为Delegate的进化使用,为了让代码简洁和优雅的呈现,C# Lambda表达式的使用功不可灭,那么依托外部条件如何动态构建C# Lambda表达式呢.下面让我们来具体的看看实施. 或许你会奇怪这个需求是如何产生的…… 首先,Lambda 在 DLinq 中承担了以往 T-SQL 的部分角色:其次,在数据库设计中,我们往往需要依

动态类型语言 vs 静态类型语言

本文内容 Dynamically Typed Language(动态类型语言) Statically Typed Language(静态类型语言) 比较 参考资料 类型检查所进行的检验处理以及实行类型的约束,可发生在编译时期(静态检查)或运行时期(动态检查).静态类型检查是在编译器所进行语义分析中进行的.如果一个语言强制实行类型规则就称此处理为强类型(或静态类型),反之称为弱类型(或动态类型). Dynamically Typed Language(动态类型语言) 所谓动态类型语言,就是类型的检

【转载】编译型语言、解释型语言、静态类型语言、动态类型语言概念与区别

编译型语言和解释型语言 1.编译型语言 需通过编译器(compiler)将源代码编译成机器码,之后才能执行的语言.一般需经过编译(compile).链接(linker)这两个步骤.编译是把源代码编译成机器码,链接是把各个模块的机器码和依赖库串连起来生成可执行文件. 优点:编译器一般会有预编译的过程对代码进行优化.因为编译只做一次,运行时不需要编译,所以编译型语言的程序执行效率高.可以脱离语言环境独立运行. 缺点:编译之后如果需要修改就需要整个模块重新编译.编译的时候根据对应的运行环境生成机器码,