仔细阅读JVMS §4.10.2.2后,我注意到以下段落:
如果对应的值都是数组引用类型,则检查它们的维度。如果数组类型具有相同的维度,则合并值是对数组类型的示例的引用,该数组类型是两个数组类型的第一公共超类型。(如果两个数组类型中的一个或两个都有基本元素类型,那么Object被用作元素类型。)[...]
[...]即使int[]和String[]也可以合并;结果是Object[],因为在计算第一个公共超类型时使用Object而不是int。int[]
不应该是Object[]
的子类型,所以这很有趣,让我们测试一下:
package test.se17;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
import java.lang.invoke.MethodHandles;
public class CastArrayDump {
public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
MethodVisitor methodVisitor;
classWriter.visit(V1_5, ACC_PUBLIC | ACC_SUPER, "test/se17/CastArray", null,
"java/lang/Object", null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "cast",
"(Ljava/lang/Object;)[Ljava/lang/Object;", null, null);
methodVisitor.visitCode();
Label end = new Label();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitTypeInsn(INSTANCEOF, "[I");
Label notint = new Label();
methodVisitor.visitJumpInsn(IFEQ, notint);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitTypeInsn(CHECKCAST, "[I");
methodVisitor.visitJumpInsn(GOTO, end);
methodVisitor.visitLabel(notint);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitTypeInsn(INSTANCEOF, "[Ljava/lang/String;");
Label notobj = new Label();
methodVisitor.visitJumpInsn(IFEQ, notobj);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitTypeInsn(CHECKCAST, "[Ljava/lang/String;");
methodVisitor.visitJumpInsn(GOTO, end);
methodVisitor.visitLabel(notobj);
methodVisitor.visitMethodInsn(INVOKESTATIC, "test/se17/CastArray",
"newIllegalArgumentException", "()Ljava/lang/IllegalArgumentException;", false);
methodVisitor.visitInsn(ATHROW);
methodVisitor.visitLabel(end);
methodVisitor.visitInsn(ARETURN);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
}
{
methodVisitor =
classWriter.visitMethod(ACC_PRIVATE | ACC_STATIC, "newIllegalArgumentException",
"()Ljava/lang/IllegalArgumentException;", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException",
"<init>", "()V", false);
methodVisitor.visitInsn(ARETURN);
methodVisitor.visitMaxs(2, 0);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
return classWriter.toByteArray();
}
public static void main(String[] args) throws IllegalAccessException, Exception {
MethodHandles.lookup().defineClass(dump());
test();
}
private static void test() {
System.out.println(CastArray.cast(new String[0]));
}
}
字符串
请注意,CastArrayDump
是针对带有cast
方法的存根CastArray
编译的,该方法具有相同的签名。
当运行这段代码时,我得到以下异常:
Exception in thread "main" java.lang.VerifyError: (class: test/se17/CastArray, method: cast signature: (Ljava/lang/Object;)[Ljava/lang/Object;) Wrong return type in function
at java.base/java.lang.ClassLoader.defineClass0(Native Method)
at java.base/java.lang.System$2.defineClass(System.java:2307)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2439)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2416)
at java.base/java.lang.invoke.MethodHandles$Lookup.defineClass(MethodHandles.java:1843)
at test.se17/test.se17.CastArrayDump.main(CastArrayDump.java:69)
型
在cast
中,控制流与end
标签合并。
在堆栈上,我们有一个String[]
或一个int[]
-应该合并到Object[]
中。
areturn指令是类型安全的,如果封闭方法具有声明的返回类型ReturnType,即引用类型,并且可以有效地将与ReturnType匹配的类型弹出传入操作数堆栈。
现在,堆栈上应该有一个Object[]
,因为我们刚刚合并到它。
但由于某种原因,情况并非如此,我们得到了“函数中返回类型错误”。
所以,我必须在JVMS中错过一些禁止这样做的东西。
在JVMS中的哪个位置说明这不起作用?
(* 要明确:我从来没有想到这将工作摆在首位 *)
1条答案
按热度按时间jhkqcmku1#
Java 7的JVM规范看起来像:
若要合并两个操作数堆栈,每个堆栈上的值的数量必须相同。堆栈上的值的类型也必须相同,除了不同类型的
reference
值可能出现在两个堆栈上的相应位置。在这种情况下,合并的操作数堆栈包含两种类型的第一个公共超类的示例的reference
。这样的reference
类型总是存在的,因为Object
类型是所有类和接口类型的超类。如果无法合并操作数堆栈,则方法验证失败。它非常简单,不包含任何关于数组类型的特殊规则。因为这个规则需要咨询JLS关于两个引用类型的公共超类,我们也可以对数组这样做,以找到,例如。
Integer[]
和Long[]
有共同的超类Number[]
,但int[]
的直接超类是Object
,它可以分配给Cloneable
和Serializable
,但不能分配给Object[]
。然后,Java 8更改了规范(注意,关于数组的这一部分是“通过类型推断进行验证”部分中唯一的更改)。
从那时起,它读起来就像你已经引用的那样:
如果对应的值都是数组引用类型,则检查它们的维度。如果数组类型具有相同的维度,则合并值是
reference
到数组类型的示例,该数组类型是两个数组类型的第一个公共超类型。到目前为止,就像以前一样。它可以在这里停止,而不是在随后的部分中自相矛盾:
(If数组类型中的一个或两个都有原始元素类型,则使用
Object
作为元素类型。请记住,对于两种类型相同的情况,没有额外的规则。因此,如果我们从字面上理解这个插入,它甚至意味着合并
int[]
和int[]
必须导致Object[]
,因为“其中一个或两个数组类型都有一个基本元素类型”。这很容易被证伪
字符串
适用于所有Java版本,不会导致类似“
iaload
与(合并)类型Object[]
一起使用”的问题。我也跳过了关于不同维度和明显例子的部分。
...甚至
int[]
和String[]
都可以合并;结果是Object[]
,因为在计算第一公共超类型时使用Object
而不是int
。这是一个特殊的情况。这一次,它不是一个有问题的措辞或过于复杂的问题导致误解,在这里,作者明确指出,它的意思是这样的,它的意思是只是......作为错误的信息可以。
这甚至违反了常识。
你已经写了“* 我从来没有想到这将工作摆在首位 *”,它应该如何?如果JVM真的允许
int[]
作为Object[]
传递,那么它在后续代码中应该做什么?规范中没有任何部分解释JVM应该如何处理它,以及应该以何种方式处理它,这将需要对实现进行重大更改。
一种选择是当后面的代码试图以
Object[]
的形式访问int[]
数组时产生错误。当然,更改JVM以允许某些内容,而只是在其他地方禁止结果是没有意义的。另一个选择是在JVM级别实现自动装箱功能。考虑到这种更改的工作量,实现这样的功能并仅在具有过时类文件版本的不常见字节码在两个代码路径上合并两个不同类型的数组时启用它将是一种奇怪的方法。
同样,在规范中插入这样一条语句,只是在其他地方添加另一条规则来解释为什么它不起作用,这将是一种奇怪的方法。所以我不认为你错过了什么;我不希望在任何地方找到这样的声明。这种明显错误的说法一开始就不应该存在。