反编译 for-each 循环

Decompiling for-each loop

反编译以下 for-each 循环的 .class 文件会产生有趣的结果。

来源 - Main.java:

public class Main {
    public static void main(String[] args) {
        String[] names = new String[3];
        int var3 = 3;

        for (String name : names) {
            System.out.println(name);
        }
    }
}

结果 - Main.class:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

public class Main {
    public Main() {
    }

    public static void main(String[] args) {
        String[] names = new String[3];
        int var3 = true;
        String[] var3 = names;
        int var4 = names.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String name = var3[var5];
            System.out.println(name);
        }

    }
}

该文件是用 IntelliJ IDEA 反编译的。

这是反编译器的错误吗?

在字节码层面上,没有局部变量的正式声明,至少不是从源代码中知道的方式。一个方法声明了同时存在的局部变量的最大数量或为它们保留的“槽”。当一个实际值分配给局部变量(通过“插槽”索引)并且至少存在到该值的最后一次读取时,局部变量就会生效。

使用这些操作,无法识别变量的作用域何时结束,或者两个具有分离作用域的变量是否共享一个槽(与对同一变量的多次赋值相比)。好吧,如果他们有完全不兼容的类型,他们的分配会给出提示。

为了帮助调试,有一个可选的代码属性提供有关声明的局部变量及其范围的提示,但这并不需要完整,也不会影响 JVM 执行字节码的方式。但是在这里,似乎该属性存在并且已被反编译器使用。

当我用 javac -g 编译您的示例代码时,我得到

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
  stack=2, locals=7, args_size=1
     0: iconst_3
     1: anewarray     #2        // class java/lang/String
     4: astore_1
     5: iconst_3
     6: istore_2
     7: aload_1
     8: astore_3
     9: aload_3
    10: arraylength
    11: istore        4
    13: iconst_0
    14: istore        5
    16: iload         5
    18: iload         4
    20: if_icmpge     43
    23: aload_3
    24: iload         5
    26: aaload
    27: astore        6
    29: getstatic     #3        // Field java/lang/System.out:Ljava/io/PrintStream;
    32: aload         6
    34: invokevirtual #4        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    37: iinc          5, 1
    40: goto          16
    43: return
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
       29       8     6  name   Ljava/lang/String;
        0      44     0  args   [Ljava/lang/String;
        5      39     1 names   [Ljava/lang/String;
        7      37     2  var3   I

声明的变量args(方法参数)、namesvar3name被分配给变量索引0126,依此顺序。

有没有声明的合成变量,

  • 在索引 3 处保存对循环正在迭代的数组的引用
  • 在索引 4 处保存数组长度
  • 在索引 5 处保存将在循环中递增的 int 索引变量

看来,反编译器有一个简单的策略来处理不包含在LocalVariableTable中的变量。它生成一个由前缀 "var" 和堆栈帧中的索引组成的名称。因此它为上述合成变量生成了名称 var3var4var5,并且不关心这些生成的名称与显式声明的名称之间是否存在名称冲突,即var3.

现在,还不清楚为什么反编译器会为 int 变量生成 true 的赋值,但知道 boolean 中没有专用的处理指令会有所帮助 Java 字节码,而是 boolean 值的处理方式与 int 值相同。它需要适当的元信息,如变量声明,以了解何时应将值解释为 boolean 值。也许,上面描述的名称冲突导致反编译器随后混淆了变量类型,最终认为值类型不是 int 并退回到将其视为 boolean。但这只是一个猜测;也可能存在完全不相关的错误。