您不能将任意类转换为隐藏类。
The 的文档defineHiddenClass https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#defineHiddenClass(byte%5B%5D,boolean,java.lang.invoke.MethodHandles.Lookup.ClassOption...)包含句子
- 任何尝试解析运行时常量池中由
this_class
,符号引用被认为解析为C
并且决议总是立即成功。
它没有明确说明的是,这是类型解析在隐藏类中结束的唯一位置。
但文中已经明确表示错误报告 JDK-8222730 https://bugs.openjdk.java.net/browse/JDK-8222730:
对于隐藏类,其指定的隐藏名称只能通过隐藏类的“this_class”常量池条目访问。
即使在隐藏类中,也不应该通过在方法或字段签名中指定其原始名称来访问该类。
我们可以检查一下。即使像这样简单的案例
public class HiddenClassLambdaTest {
public static void main(String[] args) throws Throwable {
byte[] classFileContents = HiddenClassLambdaTest.class
.getResourceAsStream("HiddenClassLambdaTest$LambdaRunner.class")
.readAllBytes();
var hidden = MethodHandles.lookup()
.defineHiddenClass(classFileContents, true, ClassOption.NESTMATE);
Runnable lambdaRunnerInstance = (Runnable)hidden.findConstructor(
hidden.lookupClass(), MethodType.methodType(void.class)).invoke();
lambdaRunnerInstance.run();
}
static class LambdaRunner implements Runnable {
LambdaRunner field = this;
@Override
public void run() {
}
}
}
已经会失败了。请注意,尝试解析原始类名是一种特殊情况LambdaRunner
隐藏类中不会失败,因为您使用现有类作为模板。所以你得到一个IncompatibleClassChangeError
or a VerifierError
由于隐藏类和现有类之间不匹配LambdaRunner
班级。当你不使用现有类的类定义时,你会得到一个NoClassDefFoundError
.
这同样适用于
static class LambdaRunner implements Runnable {
static void method(LambdaRunner arg) {
}
@Override
public void run() {
method(this);
}
}
正如引用的错误报告所述,字段和方法都不能在其签名中引用隐藏类。
一个不太直观的例子是
static class LambdaRunner implements Runnable {
@Override
public void run() {
System.out.println("" + this);
}
}
根据编译器和选项,这将失败,就像当StringConcatFactory
使用时,其行为就像调用一个方法,该方法将所有非常量部分作为参数并返回一个String
。这是在方法签名中包含隐藏类的另一种情况。
Lambda 表达式很特殊,就像这样的类
static class LambdaRunner implements Runnable {
@Override
public void run() {
Runnable runnable = () -> System.out.println("Success");
runnable.run();
}
}
编译类似于
static class LambdaRunner implements Runnable {
@Override
public void run() {
Runnable runnable = LambdaRunner::lambdaBody;
runnable.run();
}
private static void lambdaBody() {
System.out.println("Success");
}
}
它的方法签名中没有隐藏类,但必须将包含 lambda 表达式主体的方法引用为MethodReference
。在常量池中,该方法的描述引用其声明类,使用this_class
入口。因此它被重定向到隐藏类,如文档中所述。
但该项目的建设MethodType
作为MethodReference
不使用此信息来加载Class
就像类文字一样。相反,它尝试通过定义类加载器加载隐藏类,该类加载器失败并显示NoClassDefFoundError
你已经发布了。
这似乎与JDK-8130087 https://bugs.openjdk.java.net/browse/JDK-8130087这表明普通方法解析与方法不同,MethodType
作品,可以使MethodType
在仅调用该方法可以工作的地方失败。
但有可能证明,即使解决这个问题也不能解决普遍问题:
static class LambdaRunner implements Runnable {
@Override
public void run() {
var lookup = MethodHandles.lookup();
var noArgVoid = MethodType.methodType(void.class);
try {
MethodHandle mh = LambdaMetafactory.metafactory(lookup, "run",
MethodType.methodType(Runnable.class), noArgVoid,
lookup.findStatic(LambdaRunner.class, "lambdaBody", noArgVoid),
noArgVoid).getTarget();
System.out.println("got factory");
Runnable runnable = (Runnable)mh.invokeExact();
System.out.println("got runnable");
runnable.run();
}
catch(RuntimeException|Error e) {
throw e;
}
catch(Throwable e) {
throw new AssertionError(e);
}
}
private static void lambdaBody() {
System.out.println("Success");
}
}
这绕过了上述问题并调用LambdaMetafactory
手动。当被重新定义为隐藏类时,它将打印:
got factory
got runnable
Exception in thread "main" java.lang.NoClassDefFoundError: test/HiddenClassLambdaTest$LambdaRunner/0x0000000800c01400
at test/test.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:15)
Caused by: java.lang.ClassNotFoundException: test.HiddenClassLambdaTest$LambdaRunner.0x0000000800c01400
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 1 more
这表明所有障碍都已被绕过,但是当涉及到生成的实际调用时Runnable
对于持有 lambda 主体的方法,由于目标类是hidden。急切解析符号引用的 JVM 可能会更早失败,即该示例可能无法打印got runnable
then.
与旧的 JVM 匿名类不同,无法链接到隐藏类,甚至无法链接到另一个隐藏类。
正如一开始所说,底线是,你不能将任意类变成隐藏类。 Lambda 表达式并不是唯一不适用于隐藏类的功能。尝试并感到惊讶并不是一个好主意。隐藏类只能与字节码生成器结合使用,并且仅使用已知可用的功能。