反编译 for-each 循环

csi*_*guz 5 java bytecode

反编译以下 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果 - 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);
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 为什么被true分配给未使用的int
  • 为什么要var3重新声明变量?

这是代表反编译器的错误吗?

Hol*_*ger 4

在字节码级别上,没有局部变量的正式声明,至少不是从源代码中得知的方式。方法具有同时存在的局部变量的最大数量的声明或为它们保留的 \xe2\x80\x9cslots\xe2\x80\x9d 。当为局部变量分配实际值(通过 \xe2\x80\x9cslot\xe2\x80\x9d 索引)时,局部变量就会生效,并且至少存在到该值的最后一次读取时。

\n\n

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

\n\n

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

\n\n

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

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

声明的变量args(方法参数)、namesvar3、 和按顺序name分配给变量索引012、 和。6

\n\n

有没有声明的合成变量,

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

看起来,反编译器有一个简单的策略来处理LocalVariableTable. 它生成一个由堆栈帧内的前缀"var"和索引组成的名称。因此它生成了名称var3var4并且var5对于上述合成变量,\xe2\x80\x99 并不关心这些生成的名称和显式声明的名称之间存在名称冲突,即var3

\n\n

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

\n