跳转到内容

Lambda表达式的翻译

原文:https://cr.openjdk.org/~briangoetz/lambda/lambda-translation.html
翻译:张欢
备注:原文中会有一些括号、命名等细微错误,已进行更正。

本文档概述了将 lambda 表达式和方法引用从 Java 源代码转换为字节码的策略。Java 的 lambda 表达式由JSR 335指定并在OpenJDK的Lambda项目中实现。可以在Lambda状态中找到该语言功能的概述。

本文档介绍了编译器在遇到 lambda 表达式时必须生成的字节码,以及语言运行时如何参与 lambda 表达式的求值。本文档的大部分内容介绍了函数式接口转换的机制。

函数式接口是Java中 lambda 表达式的核心部分。函数式接口是具有一个非Object方法的接口,例如RunnableComparator等。(Java库多年来一直使用此类接口来表示回调。)

Lambda 表达式只能出现在将被赋值给函数接口类型的变量的地方。例如:

Runnable r = () -> { System.out.println("hello"); };

或者

Collections.sort(strings, (String a, String b) -> -(a.compareTo(b)));

编译器生成的捕获这些 lambda 表达式的代码既依赖于 lambda 表达式本身,也依赖于它被分配给的功能接口类型。

转换方案依赖于JSR 292中的几个功能,包括invokedynamic、方法句柄以及方法句柄和方法类型的增强型LDC字节码形式。由于这些无法在Java源代码中表示,因此我们的示例将使用伪语法来表示这些特性:

  • 对于方法句柄常量:MH([refKind] class-name.method-name)
  • 对于方法类型常量:MT(method-signature)
  • 对于invokedynamicINDY((bootstrap, static args...)(dynamic args...))

读者应该具备JSR 292特性的相关知识。

转换方案还假定JSR-292专家组正在为Java SE 8指定一项新功能:用于反射常量方法句柄的API。

我们可以用多种方式在字节码中表示 lambda 表达式,例如内部类、方法句柄、动态代理等。每种方法都有优点和缺点。在选择策略时,有两个相互竞争的目标:(1)通过不承诺特定策略来最大限度地提高未来优化的灵活性,与(2)在类文件表示中提供稳定性。我们可以通过使用JSR 292中的invokedynamic功能将字节码中 lambda 创建的二进制表示与运行时评估 lambda 表达式的机制分开,从而实现这两个目标。我们不是生成字节码来创建实现lambda表达式的对象(例如调用内部类的构造函数),而是描述构造 lambda 的方法,并将实际构造委托给语言运行时。该方法编码在invokedynamic指令的静态和动态参数列表中。

使用invokedynamic让我们可以将转换策略的选择推迟到运行时。运行时实现可以自由地动态选择策略来评估 lambda 表达式。运行时实现的选择隐藏在 lambda 构造的标准化(即平台规范的一部分)API后面,因此静态编译器可以发出对此API的调用,而JRE实现可以选择其首选的实现策略。invokedynamic机制允许这样做,而不会产生这种后期绑定方法可能造成的性能成本。

当编译器遇到 lambda 表达式时,它首先将 lambda 主体降低(解糖)为一个方法,该方法的参数列表和返回类型与lambda表达式相匹配,可能还会添加一些其他参数(如果有的话,用于从词法范围捕获的值)。在捕获 lambda 表达式时,它会生成一个invokedynamic调用点(CallSite),调用时会返回 lambda 正在转换到的功能接口的实例。对于给定的 lambda ,此调用点称为lambda工厂。Lambda 工厂的动态参数是从词法范围捕获的值。Lambda 工厂的引导方法是Java语言运行时库中的标准化方法,称为lambda元工厂。静态引导参数捕获编译时已知的有关 lambda 的信息(它将转换到的功能接口、解糖后的 lambda 主体的方法句柄、有关SAM类型是否可序列化的信息等)。

方法引用的处理方式与 lambda 表达式相同,只是大多数方法引用不需要解析为新方法;我们可以简单地为引用的方法加载一个常量方法句柄,并将其传递给元工厂。

将 lambda 转换为字节码的第一步是将 lambda 体解糖为方法。

解糖过程中必须做出几个选择:

  • 我们要将语法糖解糖为静态方法还是实例方法?
  • 解糖后的方法应该放在哪个类中?
  • 解糖后的方法的可访问性应该如何?
  • 解糖后的方法应该叫什么名字?
  • 如果需要进行调整来弥合 lambda 主体签名和函数式接口方​​法签名之间的差异(例如装箱、拆箱、原始扩展或缩小转换、可变参数转换等),解糖后的方法应该遵循 lambda 主体的签名、函数式接口方​​法的签名还是两者之间的签名?谁负责所需的调整?
  • 如果 lambda 从封闭范围捕获参数,那么这些参数应该如何在解糖后的方法签名中表示?(它们可以是添加到参数列表开头或结尾的单个参数,或者编译器可以将它们收集到单个“框架”参数中。)

与对 lambda 主体进行解糖相关的问题是方法引用是否需要生成适配器或“桥接”方法。

编译器将推断 lambda 表达式的方法签名,包括参数类型、返回类型和抛出的异常;我们将其称为自然签名。Lambda 表达式还有一个目标类型,它将是一个函数式接口;我们将lambda描述符称为目标类型擦除描述符的方法签名。实现函数式接口并捕获 lambda 行为的 lambda 工厂返回的值称为lambda对象

在所有条件相同的情况下,私有方法优于非私有方法,静态方法优于实例方法,最好在 lambda 表达式出现的最内层类中对 lambda 主体进行解糖,签名应与 lambda 的主体签名匹配,捕获值的额外参数应放在参数列表的前面,并且根本不要对方法引用进行解糖。然而,也有例外情况,我们可能不得不偏离这一基本策略。

要翻译的 lambda 表达式的最简单形式是它不从其封闭范围中捕获任何状态(一个“无状态”的lambda):

class A {
public void foo() {
List<String> list = ...
list.forEach( s -> { System.out.println(s); } );
}
}

lambda 的自然签名是(String)VforEach方法采用 lambda 描述符为(Object)VBlock<String>。编译器将 lambda 主体解糖为签名为自然签名的静态方法,并为解糖主体生成名称。

class A {
public void foo() {
List<String> list = ...
list.forEach( [lambda for lambda$1 as Block] );
}
static void lambda$1(String s) {
System.out.println(s);
}
}

Lambda 表达式的另一种形式涉及捕获封闭的最终(或有效最终)局部变量,和/或封闭实例的字段(我们可以将其视为捕获封闭this引用的最终内容)。

class B {
public void foo() {
List<Person> list = ...
final int bottom = ..., top = ...;
list.removeIf( p -> (p.size >= bottom && p.size <= top) );
}
}

在这里,我们的 lambda 从封闭范围捕获最终的局部变量bottomtop

解糖方法的签名将是自然签名(Person)Z,并在参数列表的前面添加一些额外的参数。编译器对于如何表示这些额外的参数有一定的自由度;它们可以单独添加,装入框架类,装入数组等。最简单的方法是单独添加它们:

class B {
public void foo() {
List<Person> list = ...
final int bottom = ..., top = ...;
list.removeIf( [ lambda for lambda$1 as Predicate capturing (bottom, top) ]);
}
static boolean lambda$1(int bottom, int top, Person p) {
return (p.size >= bottom && p.size <= top;
}
}

或者,可以将捕获的值(bottomtop)装入框架或数组中;关键是额外参数的类型(如它们出现在解糖化的 lambda 方法的签名中)与它们作为 lambda 工厂的(动态)参数出现时的类型一致。由于编译器控制这两者,并且它们是同时生成的,因此编译器在如何打包捕获的参数方面具有一定的灵活性。

Lambda 捕获将通过 invokedynamic 调用点实现,其静态参数描述 Lambda 函数体和 Lambda 描述符的特征,动态参数(如果有)则是捕获的值。调用此调用点时,它会返回一个绑定到捕获值的 Lambda 对象,该对象对应于相应的 Lambda 函数体和描述符。此调用点的引导方法是一个名为 Lambda 元工厂的指定平台方法。(我们可以为所有 Lambda 函数形式使用同一个元工厂,也可以为常见情况使用专门的版本。)虚拟机 (VM) 只会为每个捕获点调用一次元工厂;之后,它会链接该调用点并退出。调用点采用延迟链接的方式,因此从未被调用的工厂点永远不会被链接。基本元工厂的静态参数列表如下所示:

metaFactory(MethodHandles.Lookup caller, // 由VM提供
String invokedName, // 由VM提供
MethodType invokedType, // 由VM提供
MethodHandle descriptor, // lambda描述符
MethodHandle impl) // lambda体

前三个参数(callerinvokedNameinvokedType)由 VM 在调用点链接时自动堆叠。

descriptor 参数标识 lambda 表达式要转换为的函数式接口方​​法。(通过方法句柄的反射 API,元工厂可以获取函数式接口类的名称及其主要方法的名称和方法签名。)

impl 参数标识 lambda 方法,可以是解糖后的 lambda 表达式主体,也可以是方法引用中指定的方法。

函数式接口方​​法和实现方法的方法签名可能存在一些差异。实现方法可能包含与捕获的参数对应的额外参数。其余参数也可能不完全匹配;允许进行某些适配(子类型化、装箱),具体说明请参见“适配”部分。

现在我们可以描述 lambda 表达式和方法引用的函数式接口转换。我们可以将示例 A 转换为:

class A {
public void foo() {
List<String> list = ...
list.forEach(indy((MH(metaFactory), MH(invokeVirtual Block.apply),
MH(invokeStatic A.lambda$1))( )));
}
private static void lambda$1(String s) {
System.out.println(s);
}
}

因为 A 中的 lambda 函数是无状态的,所以 lambda 工厂站点的动态参数列表为空。

例如 B,动态参数列表不为空,因为我们必须向 lambda 工厂提供 bottomtop 的值:

class B {
public void foo() {
List<Person> list = ...
final int bottom = ..., top = ...;
list.removeIf(indy((MH(metaFactory), MH(invokeVirtual Predicate.apply),
MH(invokeStatic B.lambda$1))( bottom, top )));
}
private static boolean lambda$1(int bottom, int top, Person p) {
return p.size >= bottom && p.size <= top;
}
}

上述章节中的 Lambda 表达式可以转换为静态方法,因为它们不以任何方式使用封闭对象实例(不要引用 thissuper 或封闭实例的成员)。我们将使用 thissuper 或捕获封闭实例成员的 Lambda 表达式统称为实例捕获 Lambda 表达式。

非实例捕获 Lambda 表达式会被转换为私有静态方法。实例捕获 Lambda 表达式会被转换为私有实例方法。这简化了实例捕获 Lambda 表达式的解糖过程,因为 Lambda 表达式主体中的名称与解糖后的方法中的名称含义相同,并且与现有的实现技术(绑定方法句柄)完美契合。捕获实例捕获 Lambda 表达式时,接收者(this)被指定为第一个动态参数。

例如,考虑一个捕获字段 minSize 的 Lambda 表达式:

list.filter(e -> e.getSize() < minSize )

我们将其解糖为实例方法,并将接收器作为第一个捕获的参数传递:

list.forEach(INDY((MH(metaFactory), MH(invokeVirtual Predicate.apply),
MH(invokeVirtual B.lambda$1))( this )));
private boolean lambda$1(Element e) {
return e.getSize() < minSize;
}

由于 lambda 表达式会被转换为私有方法,因此在将行为方法句柄传递给元工厂时,捕获点应该加载一个常量方法句柄,其引用类型对于实例方法为 REF_invokeSpecial,对于静态方法为 REF_invokeStatic

我们可以将其解糖为私有方法,因为捕获类可以访问该私有方法,从而获得指向该私有方法的句柄,进而使元工厂能够调用该方法。(如果元工厂正在生成实现目标函数式接口的字节码,它不会直接调用方法句柄,而是通过 Unsafe.defineClass 加载这些类,而 Unsafe.defineClass 不受可访问性检查的影响。)

方法引用有多种形式,与 lambda 表达式类似,可以分为实例捕获型和非实例捕获型。非实例捕获型方法引用包括静态方法引用(例如 Integer::parseInt,使用引用类型 invokeStatic 捕获)、未绑定实例方法引用(例如 String::length,使用引用类型 invokeVirtual 捕获)以及顶层构造函数引用(例如 Foo::new,使用引用类型 invoke_newSpecial 捕获)。捕获非实例捕获型方法引用时,捕获的参数列表始终为空。

list.filter(String::isEmpty)

翻译为:

list.filter(indy((MH(metaFactory), MH(invokeVirtual Predicate.apply),
MH(invokeVirtual String.isEmpty))()))

实例捕获方法引用形式包括绑定实例方法引用(s::length,使用引用类型 invokeVirtual 捕获)、父类方法引用(super::foo,使用引用类型 invokeSpecial 捕获)和内部类构造函数引用(Inner::new,使用引用类型 invokeNewSpecial 捕获)。捕获实例捕获方法引用时,捕获的参数列表始终包含一个参数,对于父类或内部类构造函数方法引用,该参数为 this;对于绑定实例方法引用,该参数为指定的接收器。

如果要将指向可变参数方法的引用转换为非可变参数的函数式接口,编译器必须生成一个桥接方法,并捕获该桥接方法的句柄,而不是目标方法本身的句柄。该桥接方法必须处理参数类型的任何必要调整,以及从可变参数到非可变参数的转换。例如:

interface SIS {
void foo(Integer a1, Integer a2, String a3);
}
class Foo {
static void m(Number a1, Object... rest) { ... }
}
class Bar {
void bar() {
SIS x = Foo::m;
}
}

这里,编译器需要生成一个桥接函数,将第一个参数类型从 Number 转换为 Integer,并将剩余的参数收集到一个 Object 数组中:

class Bar {
void bar() {
SIS x = indy((MH(metafactory), MH(invokeVirtual IIS.foo),
MH(invokeStatic m$bridge))( ))
}
static private void m$bridge(Integer a1, Integer a2, String a3) {
Foo.m(a1, a2, a3);
}
}

解糖后的 lambda 方法有一个参数列表和一个返回类型:(A1..An) -> Ra(如果解糖后的方法是实例方法,则接收者被视为第一个参数)。函数式接口方​​法也类似地有一个参数列表和一个返回类型:(F1..Fm) -> Rf(没有接收者参数)。而工厂站点的动态参数列表包含参数类型 (D1..Dk)。如果 lambda 是实例捕获的,则第一个动态参数必须是接收者。

这些参数列表的长度必须满足以下条件:k+m == n。也就是说,lambda 函数体参数列表的长度应该等于动态参数列表和函数式接口方​​法参数列表的总和。

我们将 lambda 函数体参数列表 A1..An 拆分为 (D1..Dk H1..Hm),其中 D 参数对应于“额外”(动态)参数,H 参数对应于函数式接口参数。

我们要求 Hi 能够适配 Fii1..m)。类似地,我们要求 Ra 能够适配 Rf。类型 T 适配类型 U 的条件是:

  • T == U
  • T 是原始类型,U 是引用类型,并且 T 可以通过装箱转换转换为 U
  • T 是引用类型,U 是原始类型,并且 T 可以通过拆箱转换转换为 U
  • TU 都是原始类型,并且 T 可以通过原始类型扩展转换转换为 U
  • TU 都是引用类型,并且 T 可以强制转换为 U

适配由元工厂在链接时验证,并在捕获时执行。

为所有 lambda 表达式形式使用单个元工厂虽然实用,但似乎最好将其拆分为多个版本:

  • 一个“快速路径”版本,支持不可序列化的 lambda 表达式以及不可序列化的静态或未绑定实例方法引用;
  • 一个“可序列化”版本,支持可序列化的 lambda 表达式以及各种方法引用;
  • 如有必要,还可以添加一个“全功能”版本,支持任意组合的转换特性。

全功能版本会接受一个额外的flags参数来选择选项,可能还会接受其他特定于选项的参数。可序列化版本可能需要接受与序列化相关的额外参数。

由于元工厂并非由用户直接调用,因此不会因为存在多种实现相同功能的方式而造成混淆。通过消除不必要的参数,可以减小类文件的大小。快速路径选项降低了虚拟机对 lambda 转换操作的固有要求,使其能够被视为“装箱”操作,从而简化了拆箱优化。

我们的动态翻译策略需要动态序列化策略。例如,如果我们希望从今天为每个 lambda 表达式创建一个内部类,切换到明天使用动态代理,那么今天序列化的 lambda 对象在明天反序列化时必须转换为动态代理。这可以通过为 lambda 表达式定义一个中性的序列化形式,并使用 readResolvewriteReplace 在 lambda 对象和序列化形式之间进行转换来实现。序列化形式必须包含通过元工厂重新创建对象所需的所有信息。例如,序列化形式可能如下所示:

public interface SerializedLambda extends Serializable {
// 捕获上下文
String getCapturingClass();
// SAM描述符
String getDescriptorClass();
String getDescriptorName();
String getDescriptorMethodType();
// 实现
int getImplReferenceKind();
String getImplClass();
String getImplName();
String getImplMethodType();
// 动态参数 -- 这些也都是需要序列化的
Object[] getCapturedArgs();
}

在此,SerializedLambda 接口提供了原始 Lambda 捕获点的所有信息。捕获可序列化 Lambda 时,元工厂必须返回一个实现了 writeReplace 方法的对象,该对象返回一个 SerializedLambda 实现,该实现包含一个 readResolve 方法,用于重新创建 Lambda 对象。

一个挑战是反序列化代码需要为实现方法构造一个方法句柄。虽然序列化形式提供了所有必要的标称信息(种类、类、名称和类型),因此可以使用 MethodHandles.Lookup 上公开的 findXxx 方法之一来构造,但 SerializedLambda 实现可能无法访问 lambda 方法或被引用的方法(可能是因为该方法本身不可访问,也可能是因为其所在的类不可访问)。

对于 lambda 表达式的源工厂站点来说,这不是问题,因为实现方法的句柄是使用该类的可访问性权限加载的。但是,为了避免引入安全隐患,我们希望尽量减少反序列化过程中使用的任何提升权限,并且当然不希望修改 JVM 的可访问性规则。

一种可行的方法是确保可序列化 lambda 表达式或方法引用的实现方法是公共类的公共方法。这可能已经是事实(方法引用 String::length),或者很容易实现(对于公共类,我们可以将 lambda 表达式反糖化为公共方法)。但这同样不可取,因为它会将内部实现暴露为公共方法,并且与我们对不可序列化 lambda 表达式的转换方式不一致。在某些情况下,这还需要一些复杂的技巧,例如,如果 lambda 表达式位于非公共类中,则需要创建一个公共的“边车”类。(在某种程度上,“暴露为公共”是不可避免的,因为序列化本身就是如此——为类提供一个外部的公共构造函数。但我们希望尽量减少这种暴露。)

更好的方法是将反序列化委托回执行 lambda 表达式捕获的类。一个关键的安全挑战是,防止反序列化机制允许攻击者通过构造篡改的字节流来构造一个调用任意私有方法的 lambda 对象。序列化自然地为特定的(函数式接口、行为方法、捕获的参数类型)组合公开了“构造函数”;通过委托给捕获类,它可以在继续操作之前验证字节流是否代表有效的组合之一。一旦验证了组合,它就可以通过元工厂调用来构造 lambda 表达式,并使用自身的访问权限加载方法句柄。

为此,捕获类应该有一个可以从序列化层调用的辅助方法,类似于 readObjectwriteObjectreadResolvewriteReplace 方法。我们将此方法称为 $deserialize$(SerializedLambda)。序列化层唯一需要的特权操作就是调用这个(可能是私有的)方法。

编译捕获可序列化 lambda 表达式的类时,编译器知道哪些(函数式接口、行为方法、捕获的参数类型)组合已被捕获为可序列化 lambda 表达式。$deserialize$ 方法应该只支持对这些组合的反序列化。

考虑以下类,它捕获两个可序列化的 lambda 表达式:

class Foo {
void moo() {
SerializableComparator<String> byLength = (a,b) -> a.length() - b.length();
SerializablePredicate<String> isEmpty = String::isEmpty;
...
}
}

我们可以这样这样翻译:

class Foo {
void moo() {
SerializableComparator<String> byLength
= indy(MH(serializableMetafactory), MH(invokeVirtual SerializableComparator.compare),
MH(invokeStatic lambda$1))());
SerializablePredicate<String> isEmpty
= indy(MH(serializableMetafactory), MH(invokeVirtual SerializablePredicate.apply),
MH(invokeVirtual String.isEmpty)());
...
}
private static int lambda$1(String a, String b) { return a.length() - b.length(); }
private static $deserialize$(SerializableLambda lambda) {
switch(lambda.getImplName()) {
case "lambda$1":
if (lambda.getSamClass().equals("com/foo/SerializableComparator")
&& lambda.getSamMethodName().equals("compare")
&& lambda.getSamMethodDesc().equals("...")
&& lambda.getImpleReferenceKind() == REF_invokeStatic
&& lambda.getImplClass().equals("com/foo/Foo")
&& lambda.getImplDesc().equals(...)
&& lambda.getInvocationDesc().equals(...))
return indy(MH(serializableMetafactory),
MH(invokeVirtual SerializableComparator.compare),
MH(invokeStatic lambda$1))(lambda.getCapturedArgs()));
break;
case "isEmpty":
if (lambda.getSamClass().equals("com/foo/SerializablePredicate"))
&& lambda.getSamMethodName().equals("apply")
&& lambda.getSamMethodDesc().equals("...")
&& lambda.getImpleReferenceKind() == REF_invokeVirtual
&& lambda.getImplClass().equals("java/lang/String")
&& lambda.getImplDesc().equals(...)
&& lambda.getInvocationDesc().equals(...))
return indy(MH(serializableMetafactory),
MH(invokeVirtual SerializablePredicate.apply),
MH(invokeVirtual String.isEmpty)(lambda.getCapturedArgs));
break;
}
throw new ...;
}
}

$deserialize$ 方法知道哪些 lambda 表达式已被此类捕获,因此可以检查提供的序列化形式是否与列表匹配,然后使用相同的调用点重建 lambda 表达式,该调用点可以与捕获点共享相同的引导索引。(或者,通过将捕获操作分解为私有方法,它可以共享相同的实际捕获点,从而共享相同的链接状态;这可以简化下文“类缓存”部分提出的一些问题。)

如果恶意调用者诱骗我们反序列化恶意字节流,则该反序列化仅对编译单元中实际是 lambda 转换目标的方法有效,而如果我们将其转换为可序列化的内部类,则会暴露这些方法。由于它与解糖方法位于同一编译单元中,因此不会引入额外的名称不稳定性(与重新编译有关)。

这使得 lambda 表达式主体可以使用简单通用的解糖策略——我们对可序列化和不可序列化的 lambda 表达式都使用相同的策略。它保留了将所有反糖化的 lambda 表达式主体设为私有的能力,无需使用 sidecar 类或辅助功能桥接方法,并且唯一需要特权的操作是调用 $deserialize$

为了降低遭受类加载攻击的风险(攻击者创建序列化的 lambda 表达式描述,意图强制加载该类以利用其静态初始化器的副作用),SerializedLambda 接口最好完全使用类名标识符,而不是 Class 对象。

在许多可能的转换策略中,我们需要生成新的类。例如,如果我们采用“每个 lambda 函数生成一个类”(在运行时而非编译时生成内部类)的方式,那么我们会在首次调用给定的 lambda 函数工厂站点时生成该类。之后,对该 lambda 函数工厂站点的后续调用将重用首次调用时生成的类。

对于可序列化的 lambda 函数,类生成可以在两个位置触发:捕获站点和 $deserialize$ 代码中对应的工厂站点。理想情况下(但并非必须),无论哪个路径先被触发,通过这两个路径生成的对象都应具有相同的类。这就要求每个 lambda 函数捕获站点都有一个唯一的键,并且对于给定的可序列化 lambda 函数,两个捕获站点之间共享一个缓存。

class SerializationExperiment {
interface Foo extends Serializable { int m(); }
public static void main(String[] args) {
Foo f1, f2;
if (args[0].equals("r")) {
// 读取文件'foo.ser'并反序列化到f1中
}
f2 = () -> 3;
if (args[0].equals("w")) {
// 序列化f2并写到文件'foo.ser'中
// 读取文件'foo.ser'并反序列化到f1中
}
assert f1.getClass() == f2.getClass();
}
}

如果我们运行两次程序:

java -ea SerializationExperiment w
java -ea SerializationExperiment r

如果无论类循环发生在首次反序列化还是首次调用元工厂时,运行都能成功,那就再好不过了。

可序列化会给 lambda 表达式带来一些额外的开销,因为 lambda 对象需要携带足够的状态才能有效地为元工厂重新创建静态和动态参数列表。这可能意味着实现类中需要额外的字段、额外的构造函数初始化工作,以及对转换策略的限制(例如,我们不能使用方法句柄代理,因为生成的对象不会实现所需的 writeReplace 方法)。因此,最好将可序列化的 lambda 表达式单独处理,而不是让所有 lambda 表达式都可序列化,并将这些开销施加到所有 lambda 表达式上。

一个函数式接口实际上可以包含多个非Object方法,因为它可能包含桥接方法。例如,在下面的函数式接口类型 B 中:

interface A<T> { void m(T t); }
interface B extends A<String> { void m(String s); }

B 的主要方法是 m(String),但 B 还有一个方法 m(Object),它是 m(String) 的桥接方法。(否则,如果将 B 强制转换为 A 并对结果调用 m,则会失败。)

当我们将 lambda 表达式转换为实现函数式接口(例如 B)的对象时,必须确保所有桥接方法以及主要方法都正确连接,并进行适当的参数或返回类型转换。通过恶意字节码生成或单独的编译产物,也可能找到编译时函数式接口中不存在的“额外”方法。与其执行完整的 JLS 桥接计算算法并仅桥接这些方法,我们可以采用 MethodHandleProxy 的捷径,即桥接所有与主要方法名称和参数数量相同的方法。 (如果发现其中任何一种与主方法不兼容,则会在调用时抛出 ClassCastException 异常,其信息量仅略低于通常会抛出的链接错误。)我们可以让编译器在元工厂中包含一个编译时已知的有效桥接签名列表,但这会增加类文件的大小,而收益甚微。

通常,lambda 对象的 toString 方法继承自 Object 类。但是,对于指向公共非合成方法的引用,我们可能希望根据实现方法中的类名和方法名来实现 toString 方法。例如,对于转换为 IntFnString::size,我们可能需要让 toString 返回 String::size()java.lang.String::size()String::size() as IntFn 等。

待办事项:如果我们支持命名 lambda 表达式的概念,我们可能希望 toString 的结果取决于 lambda 表达式的名称,在这种情况下,名称必须以某种方式传递给元工厂。