Java 14 中的模式匹配

Java 14使用模式匹配来简化 Java 中运算符实例的使用,对instanceof使用模式匹配可以简化Java中instanceof运算符的使用,从而使代码更安全,更容易编写。

public class LambdaExample {
  private static final String HELLO = "Hello World!";

  public static void main(String[] args) throws Exception {
    Runnable r = () -> System.out.println(HELLO);
    Thread t = new Thread(r);
    t.start();
    t.join();
  }
}

熟悉内部类的可能会猜测lambda实际上只是Runnable匿名实现的语法糖。

但是,其实编译上会生成一个文件class文件:LambdaExample.class,而内部类是没有这种类文件的。

所以lambda不是内部类。 而是某种机制。

通过javap -c -p反编译字节码可以清楚了解两件事。

首先,lambda主体已被编译为私有的静态方法,该方法出现在主类中:

private static void lambda$main$0();
  Code:
    0: getstatic   #7         // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc      #9         // String Hello World!
    5: invokevirtual #10         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: return

私有方法的签名与lambda的签名匹配。

public class StringFunction {
  public static final Function<String, Integer> fn = s -> s.length();
}

方法用一个字符串并返回一个整数,与接口方法的签名匹配。

private static java.lang.Integer lambda$static$0(java.lang.String);
  Code:
    0: aload_0
    1: invokevirtual #2         // Method java/lang/String.length:()I
    4: invokestatic #3         // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    7: areturn

关于字节码的第二件事要注意的是main方法的形式:

public static void main(java.lang.String[]) throws java.lang.Exception;
  Code:
    0: invokedynamic #2, 0       // InvokeDynamic #0:run:()Ljava/lang/Runnable;
    5: astore_1
    6: new      #3         // class java/lang/Thread
    9: dup
   10: aload_1
   11: invokespecial #4         // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
   14: astore_2
   15: aload_2
   16: invokevirtual #5         // Method java/lang/Thread.start:()V
   19: aload_2
   20: invokevirtual #6         // Method java/lang/Thread.join:()V
   23: return

字节码以invokedynamic调用开始。 此操作码已添加到版本7的Java中(这是有史以来唯一添加到JVM字节码的操作码)。

理解此代码中的invokedynamic调用的最直接方法是将其视为对工厂方法的异常形式的调用。 

方法调用返回实现Runnable的实例。 确切的类型没有在字节码中指定,从根本上来说没有关系。

实际类型在编译时不存在,将在运行时按需创建。 

为了更好地说明这一点,将讨论三种共同使用此功能的机制:调用点,方法句柄和引导程序。

Call sites

字节码中发生方法调用指令的位置称为调用点。

Java字节码传统上有四种操作码,分别处理方法调用的不同情况:

静态方法,“常规”调用(可能涉及方法重写的虚拟调用)。

接口查找和“特殊”调用(对于不需要覆盖解析的情况) ,如:超类调用和私有方法)。

动态调用比通过提供一种机制来使动态调用更进一步,通过这种机制,可以在每个调用点的基础上决定实际调用哪种方法。

在这里,invokedynamic调用站点在Java堆中表示为CallSite对象。

自Java 1.1以来,Java就一直使用反射API做类似的事情,其类型为Method,在这种情况下为Class。

Java在运行时具有许多动态行为,因此,Java现在正在对调用点及运行时类型信息进行建模的想法就不足为奇了。

到达invokedynamic指令时,JVM会找到相应的调用站点对象(如果以前从未到达过此调用站点,则它会创建一个)。

呼叫站点对象包含一个方法句柄,它是一个代表我实际要调用的方法的对象。

调用点对象必需是间接的,允许关联的调用目标(即方法句柄)随时间变化。

CallSite(抽象的)有三个可用的子类:ConstantCallSite、MutableCallSite和VolatileCallSite。

三个子类型都有公共构造函数,基类仅有专用构造函数。

这意味着CallSite不能由用户代码直接子类化,但是可以对子类型进行子类化。

如,JRuby将invokedynamic用作其实现的一部分,并继承了MutableCallSite子类。

注意:某些invokedynamic调用点实际上只是延迟计算的,它们所针对的方法在第一次执行后就不会改变。

这是ConstantCallSite非常常见的用例,其中包括lambda表达式。

这意味着在程序的整个生命周期中,非恒定调用点可以有许多不同的方法句柄作为目标。

Method handles

反射是一种用于执行运行时技巧的强大技术,但它有许多设计缺陷,反射的一个关键问题是性能,尤其是因为即时调用(JIT)编译器难以内联反射调用。

内联在几个方面对JIT编译非常重要,其中最重要的一点是内联通常是首次应用优化,且为其他技术(如:转义分析和无效代码消除)打开了大门。

第二个问题是,每次遇到Method.invoke()调用点时,都会连接反射调用。

这意味着,如:执行安全访问检查,是非常浪费的,因为在第一次调用时检查是成功还是失败,已经有明确定义了,在程序的整个生命周期都是有效的。

然而,反射却又一次又一次地做这类连接。

因此,反射的重新链接和浪费CPU时间而招致许多不必要的成本。

为解决这些问题(及其他问题),Java 7引入了一个新的API,即java.lang.invoke,由于它引入的主类的名称,通常将其称为方法句柄。

方法句柄(MH)是Java的类型安全函数指针版本。

是一种引用代码可能要调用方法的方法,类似于Java反射中的Method对象。 

MH具有与反射相同的方法执行底层方法的invoke()方法。

一方面,MH实际上是一种更有效的反射机制,它更接近直接调用。

反射API中由对象表示的内容都可以转换成等效的MH。如,可以使用Lookup.unreflect()将反射方法对象转换为MH。创建的MH是访问基础方法的更有效方法。

可以通过MethodHandles类中的辅助方法以多种方式来修改MH。

通常,方法连接需要类型描述符的精确匹配。但是,MH上的invoke()方法具有特殊的多态签名,无论调用方法的签名如何,都允许进行连接。

在运行时,invoke()调用点上的签名应看起来像正在直接调用引用的方法一样,避免了反射调用发生的类型转换和自动装箱成本。

由于Java是一种静态类型的语言,因此出现了一个问题,当使用这种动态机制时,可以保留多少类型安全性。 

MH API通过使用一种称为MethodType的类型来解决此问题,该类型是方法参数不变表示:方法的签名。

MH的内部实现在Java 8的生命周期中进行了更改。

新的实现称为lambda形式,性能有了显着的提升,MH在许多用例中都比反射更好。

Bootstrapping

字节码指令流中第一次遇到每个特定的invokedynamic调用点时,JVM不知道它针对的是哪种方法。 实际上,没有与该指令关联的调用点对象。

调用点需要被引导,并且JVM通过运行引导方法(BSM)生成并返回调用点对象来实现此目的。

每个invokedynamic调用点都有与其关联的BSM,该BSM存储在类文件的单独区域中。 这些方法允许用户代码在运行时以编程方式确定连接。

反编译一个invokedynamic调用表明,它有以下形式:

0: invokedynamic #2, 0

在类文件的常量池中,条目#2是类型CONSTANT_InvokeDynamic的常量。 常量池的相关部分是

#2 = InvokeDynamic   #0:#31
  ...
 #31 = NameAndType    #46:#47    // run:()Ljava/lang/Runnable;
 #46 = Utf8        run
 #47 = Utf8        ()Ljava/lang/Runnable;

常数中存在0是一个提示。 常量池条目从1开始编号,因此0会提醒实际的BSM位于类文件的另一部分。

对于lambda,NameAndType条目采用特殊形式。 名称是任意的,但是类型签名包含一些有用的信息。

返回类型对应于invokedynamic工厂的返回类型。 它是lambda表达式的目标类型。 同样,参数列表由lambda捕获的元素类型组成。 对于无状态lambda,返回类型将始终为空。 仅Java闭包将存在参数。

BSM至少接受三个参数并返回CallSite。 标准参数是以下类型:

MethodHandles.Lookup:发生调用点类上的查找对象。

字符串:NameAndType中提到的名称。

MethodType:NameAndType的已解析类型描述符。

这些参数之后是BSM所需的其他参数。 这些在文档中称为附加静态参数。

BSM的一般情况允许非常灵活的机制,非Java语言实现者则使用此机制。 

但是,Java语言没有提供用于生成任意invokedynamic调用点的语言级别的构造。

对于Lambda表达式,BSM采用一种特殊形式,为了充分理解该机制的工作原理,我将对其进行更仔细的研究。

解码lambda的bootstrap方法

对Javap使用-v参数可查看引导程序方法。 

因为引导程序方法位于类文件的特殊部分中,并且将引用返回到主常量池中。

对于这个简单的Runnable示例,该部分有一个引导程序方法:

BootstrapMethods:
 0: #28 REF_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:
   #29 ()V
   #30 REF_invokeStatic LambdaExample.lambda$main$0:()V
   #29 ()V

此调用点的引导方法是常量池中的条目#28。 

这是MethodHandle类型的条目(一种常量池类型)。 现在,将其与字符串函数示例的情况进行比较:

0: #27 REF_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:
   #28 (Ljava/lang/Object;)Ljava/lang/Object;
   #29 REF_invokeStatic StringFunction.lambda$static$0:(Ljava/lang/String;)Ljava/lang/Integer;
   #30 (Ljava/lang/String;)Ljava/lang/Integer;

将用作BSM的方法句柄与静态方法LambdaMetafactory.metafactory(...)相同。

更改的部分是方法参数。 这些是lambda表达式的附加静态参数,其中有三个。 

它们代表了lambda的签名以及lambda实际最终调用目标的方法句柄:lambda主体。

第三个静态参数是签名的擦除形式。

让我们将代码跟随到java.lang.invoke中,看看平台如何使用metafactories动态地旋转实际为lambda表达式实现目标类型的类。

The lambda metafactories

BSM对此静态方法进行调用,该方法最终返回调用点对象。 当执行invokedynamic指令时,调用点中包含的方法句柄将返回实现lambda目标类型的类的实例。

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();
}

查找对象对应于invokedynamic指令所在的上下文。 

调用的名称和类型由VM提供,并且是实现的详细信息。 最后三个参数是BSM的其他静态参数。

在当前的实现中,元工厂将代码委派给使用ASM字节码库的内部阴影副本来启动实现目标类型的内部类。

如果lambda没有从其封闭范围中捕获任何参数,则结果对象是无状态的,因此该实现将通过预先计算单个实例进行优化-有效地使lambda的实现类成为单例:

jshell> Function<String, Integer> makeFn() {
  ...>  return s -> s.length();
  ...> }
| created method makeFn()

jshell> var f1 = makeFn();
f1 ==> $Lambda$27/0x0000000800b8f440@533ddba

jshell> var f2 = makeFn();
f2 ==> $Lambda$27/0x0000000800b8f440@533ddba

jshell> var f3 = makeFn();
f3 ==> $Lambda$27/0x0000000800b8f440@533ddba

结论

本文探讨了有关JVM如何实现对lambda表达式的支持的详细细节。 

在此过程中,讨论了invokedynamic和方法处理API。 这是现代JVM平台的主要部分的两项关键技术。 这两种机制都在整个生态系统中得到了越来越多的使用。 在Java 9及更高版本中,invokedynamic已用于实现新形式的字符串连接。

原文:

https://blogs.oracle.com/javamagazine/behind-the-scenes-how-do-lambda-expressions-really-work-in-java