Decompiling the .class file of the following for-each loop produces interesting results.
Source - 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);
}
}
}
Result - 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);
}
}
}
The file was decompiled with IntelliJ IDEA.
- Why was
true
assigned to the unusedint
? - Why was the
var3
variable redeclared?
Is this a mistake on behalf of the decompiler?
On the bytecode level, there are no formal declarations of local variables, at least not the way known from source code. A method has a declaration of the maximum number of local variables existing at the same time or “slots” to reserve for them. A local variable comes into life when an actual value is assigned to it (by “slot” index) and exists at least to the last read of that value.
With these operations, it is impossible to recognize when a variable’s scope ends or whether two variables with disjunct scopes share a slot (compared to multiple assignments to the same variable). Well, if they have completely incompatible types, their assignments give a hint.
To help debugging, there is an optional code attribute providing hints about declared local variables and their scope, but this is not required to be complete and won’t affect the way the JVM will execute the bytecode. But here, it seems the attribute was present and has been used by the decompiler.
When I compile your example code with javac -g
, I get
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
The declared variables args
(the method parameter), names
, var3
, and name
were assigned to the variable indices 0
, 1
, 2
, and 6
, in that order.
There are synthetic variables without a declaration,
- at index
3
to hold a reference to the array the loop is iterating over - at index
4
to hold the array length - at index
5
to hold theint
index variable that will get incremented in the loop
It seems, the decompiler has a simple strategy to deal with variables not contained in the LocalVariableTable
. It generates a name consisting of the prefix "var"
and the index within the stack frame. So it generated the names var3
, var4
and var5
for the synthetic variables described above and didn’t care that there was a name clash between these generated names and the explicitly declared names, i.e. var3
.
Now, it’s not clear why the decompiler generates an assignment of true
for an int
variable, but it helps to know that there are no dedicated boolean
processing instructions in Java bytecode, but rather boolean
values are processed the same way as int
values. It requires appropriate meta information, like variable declarations, to understand when values should get interpreted as boolean
values. Perhaps, the name clash described above led the decompiler to confuse the variable types afterwards, to eventually consider the value type not to be an int
and fall back to treat it as boolean
then. But that’s just a guess; there could be an entirely unrelated bug too.