Java:方法名称/签名解析是否静态完成(编译时)?

Ash*_*ish 5 java compiler-construction compilation

今天我遇到了一个有趣的问题,我认为这在 Java 中是不可能的。我针对 jgroups 2.6 版本编译了 java 代码,但在运行时使用了 2.12 版本(tomcat Web 应用程序部署)。我收到以下错误

org.jgroups.Message.<init>(Lorg/jgroups/Address;Lorg/jgroups/Address;Ljava/io/Serializable;)
Run Code Online (Sandbox Code Playgroud)

假设 API 从那时起会发生变化,我想将我的代码移植到 jgroups-2.12,但令我惊讶的是,代码用 jgroups-2.12 编译得很好,当我替换新的 jar 时(没有更改代码中的一行,只是针对 jgroups-2.12 而不是 jgroups-2.6 进行编译),它工作得很好。

后来我发现Message(Address, Address, Serializable)2.6中的构造函数在2.12中改为了Message(Address, Address, Object)。这意味着在运行时,JVM 试图找到完全相同的方法,但未能成功。

这是否意味着 Java 编译器在编译时嵌入了精确的方法名称和精确的参数,而具有更广泛参数的方法将不起作用?

Jon*_*eet 6

是的,这完全正确 - 确切的签名是在编译时绑定的,这就是字节码中包含的内容。

\n\n

事实上,这甚至包括返回类型,它不包含在用于重载目的的签名中。

\n\n

从根本上讲,如果您对现有公共 API 成员进行任何更改,这将是一个重大更改。您可以进行一些仅针对语言的更改,例如将String[]参数更改为String...参数,或引入泛型(在某些情况下,如果擦除与以前的代码兼容),但仅此而已。

\n\n

Java 语言规范的第 13 章全部是关于二进制兼容性的 - 阅读该章以获取更多信息。但特别是,来自第 13.4.14 节

\n\n
\n

更改方法或构造函数的形式参数名称不会影响预先存在的二进制文件。更改方法的名称、方法或构造函数的形参类型,或者在方法或构造函数声明中添加参数或从中删除参数,都会创建具有新签名的方法或构造函数,并具有以下综合效果:删除具有旧签名的方法或构造函数并添加具有新签名的方法或构造函数(请参阅\xc2\xa713.4.12)。

\n
\n


Phi*_*ler 5

这是否意味着 Java 编译器在编译时嵌入了精确的方法名称和精确的参数,而具有更广泛参数的方法将不起作用?

确切地。您还可以从收到的错误消息中看到这一点:

org.jgroups.Message.<init>(Lorg/jgroups/Address;Lorg/jgroups/Address;Ljava/io/Serializable;)
Run Code Online (Sandbox Code Playgroud)

完整的签名包含在这里,运行时会寻找完美的匹配。

在其他几种情况下,更改 API 会破坏 Java 中的二进制兼容性,但不会破坏源兼容性,例如,当您将基本类型更改为其盒装变体时,反之亦然。正如 Jon 所指出的,只有泛型的更改(但不是所有更改)和使用 VarArgs 语法不会影响运行时方法解析,因为两者都只是编译器功能,不会影响字节码。

这也意味着,当您在新的库版本中引入方法的重载时,该重载将仅由使用新版本编译的调用者使用。旧的二进制文件仍然会调用旧的方法,即使它们的参数类型更适合新的重载。

因此,对于库设计者来说,有时建议不要更改现有方法的签名,而只添加新的重载(并让旧方法转发到新方法,以便调用哪一个方法并不重要)。当然,缺点是所有这些重载都掩盖了真正的 API,并使理解 API 变得更加困难。