为什么 javac 在 class 上使用后缀“++”运算符创建一个未使用的变量?

Why is javac creating an unused variable with the postfix "++" operator on a class?

受到 this question 的启发,我开始了一些研究。

我能够确定使用基元,前缀 ++i 只是被编译器重写为 i++

之前:

public class PrefixIncrement {
    public static void main(String args[]) {
        for(Integer i = 0; i < 100; ++i) {
            System.out.println(i);
        }   
    }
}

用jd-gui 0.36反​​编译:

import java.io.PrintStream;

public class PrefixIncrement
{
  public static void main(String[] args)
  {
    for (int i = 0; i < 100; i++) {
      System.out.println(i);
    }
  }
}

好的。所以那部分得到了回答。但是后来我偶然发现了当我们使用 Integer class 时会发生什么,这次是使用 Postfix:

之前:

public class PostfixIncrement {
    public static void main(String args[]) {
        for( Integer i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

反编译后:

import java.io.PrintStream;

public class PostfixIncrement
{
  public static void main(String[] args)
  {
    Integer localInteger1;
    Integer localInteger2;
    for (Integer i = Integer.valueOf(0); i.intValue() < 100; localInteger2 = i = Integer.valueOf(i.intValue() + 1))
    {
      System.out.println(i);localInteger1 = i;
    }
  }
}

编译器似乎用“i++”做了一些愚蠢的事情。它创建了两个新的整数 classes 并创建了一系列逻辑上不必要的赋值:

localInteger2 = i = Integer.valueOf(i.intValue() + 1);

localInteger1 似乎从未被使用过。它只是分配在堆栈上。为什么 javac 这样做?

$ java -version
java version "1.7.0_60"
Java(TM) SE Runtime Environment (build 1.7.0_60-b19)
Java HotSpot(TM) 64-Bit Server VM (build 24.60-b09, mixed mode)

===================根据请求:javap 输出===================== ==

后缀增量使用 Integer class.

{
  public com.foo.PostfixIncrement();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>
":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Lcom/matt/PostfixIncrement;

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_0
         1: invokestatic  #2                  // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
         4: astore_1
         5: aload_1
         6: invokevirtual #3                  // Method java/lang/Integer.intVal
ue:()I
         9: bipush        100
        11: if_icmpge     40
        14: getstatic     #4                  // Field java/lang/System.out:Ljav
a/io/PrintStream;
        17: aload_1
        18: invokevirtual #5                  // Method java/io/PrintStream.prin
tln:(Ljava/lang/Object;)V
        21: aload_1
        22: astore_2
        23: aload_1
        24: invokevirtual #3                  // Method java/lang/Integer.intVal
ue:()I
        27: iconst_1
        28: iadd
        29: invokestatic  #2                  // Method java/lang/Integer.valueO
f:(I)Ljava/lang/Integer;
        32: dup
        33: astore_1
        34: astore_3
        35: aload_2
        36: pop
        37: goto          5
        40: return
      LineNumberTable:
        line 5: 0
        line 6: 14
        line 5: 21
        line 8: 40
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               5      35     1     i   Ljava/lang/Integer;
               0      41     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
           frame_type = 252 /* append */
             offset_delta = 5
        locals = [ class java/lang/Integer ]
           frame_type = 250 /* chop */
          offset_delta = 34

}

前缀++的int版本的语义是:++i更简单:i = i + 1; return i.

i++ 意味着更复杂的事情:int i$ = i; i = i + 1; return i$.

当 int 包装在 Integer 中时,使用的编译器显然不会生成足够智能的代​​码。它实际上保持了 i 的旧值(上面的 i$)。没有理由那么愚蠢,因为 i++ 的结果没有在那个位置使用。

for (...; ...; (void)[[ i++ ]] ) {

在第一个实例中生成

Integer i$ = i;
i = Integer.valueOf(i.intValue() + 1);
"return" i$ to a void context
drop variable i$ where i$ is unused

应该很容易减少到:

i = Integer.valueOf(i.intValue() + 1);

我怀疑这两个临时变量 localInteger1localInteger2 是为处理一般 boxing/unboxing 规则而引入的占位符。在不查看反编译字节码的情况下,这可能会揭示一些在重新解释编译后的 java 代码中没有的细节,并且在不查看其他一些不那么微不足道的情况的情况下,我只是在推测。

然而,很明显它们在循环 ForUpdate 表达式之前和之后都保留了 i 的值。

考虑这个案例:

Integer i;
for( i = 0; i < 100; i++) {
   System.out.println(i);
}
System.out.println(i); // what is the value of `i` here?

这在 Java 代码中与基元的情况相同,除了声明的类型是 Integer 而不是 int

编译器必须在循环执行期间和之后跟踪 i 的值,此时它将是 100。我怀疑在某些情况下实现循环和处理原语 int 可以轻松完成,但处理对象引用更新包括更复杂的情况,因此需要两个临时值。

正如您在 class 的 javap 输出中看到的(使用 javac 1.8.0_60 编译),变量实际上并不存在:

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
Code:
  stack=2, locals=2, args_size=1
     0: iconst_0
     1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     4: astore_1
     5: aload_1
     6: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
     9: bipush        100
    11: if_icmpge     34
    14: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
    17: aload_1
    18: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    21: aload_1
    22: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
    25: iconst_1
    26: iadd
    27: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    30: astore_1
    31: goto          5
    34: return

(局部变量与*LOAD*STORE指令一起使用,如您所见,只涉及一个。)

问题似乎是由反编译器引起的,它可能不太支持 for 语句中的装箱。所以 javac 没有做错,只是反编译器的错。无需担心性能影响或任何事情,除非您计划进行逆向工程并保存反编译代码:P.