【问题标题】:Why is this loop changed?为什么这个循环改变了?
【发布时间】:2019-05-15 15:33:51
【问题描述】:

我刚刚遇到了我班的这个反编译的类文件:

我的班级

while ((line = reader.readLine()) != null) {
    System.out.println("line: " + line);
    if (i == 0) {
        colArr = line.split(Pattern.quote("|"));

    } else {
        i++;
    }
}

while 循环已在类文件中更改为 for 循环:

反编译的 MyClass

for (String[] colArr = null; (line = reader.readLine()) != null; ++i) {
    System.out.println("line: " + line);
    if (i == 0) {
        colArr = line.split(Pattern.quote("|"));
    } else {
    }
}

为什么这个循环被改为for? 我认为这可能是编译器优化代码的另一种方式,我可能是错的。 我只是想知道是不是,for 循环比while 循环或其他循环有什么优势?
此类代码优化的类别是什么?

【问题讨论】:

  • 很难从源代码中分辨出来,但它可能只是反编译器的一个选择来向您展示这个版本。请注意,反编译器必须“猜测”一下——它会选择一个可能导致给定字节码的源代码。
  • @KumarAnkit - 不一定是优化,不。源结构和字节码之间没有一对一的关系。这就是 Hulk 所说的“反编译器必须‘猜测’一点”的意思。
  • @KumarAnkit 人们试图解释的是,也许根本没有优化。尝试使用谷歌翻译用英语翻译印地语(或任何你的地方方言)句子,然后再翻译回印地语。如果它与开头的句子相同,你会很幸运。这是一样的东西,明白了吗?
  • @KumarAnkit - for(和 while 等)在字节码级别不存在。它是跳转指令、赋值等。一个反编译器可能会查看一些字节码并说“看起来像 for”,而另一个可能会看着它并说“看起来像 while”。 “那么,这是否意味着这些循环在字节码级别是相同的?” 不一定。如果您通过反编译器编译代码输出,则不一定最终得到相同的字节码。事实上,我怀疑你很少会这样做。
  • OP 一定对我们隐瞒了什么。这两个 sn-ps 不等价。第一个永远不会更新i一旦等于0,所以在合理的情况下它可能会多次设置colArr的值。第二个将只更新一次colArr

标签: java loops optimization


【解决方案1】:

在这种情况下,将while() 更改为for() 不是优化。根本无法从字节码中知道源代码中使用了哪一个。

有很多情况:

while(x)

等同于:

for(;x;)

假设我们有三个类似的 java 应用程序 - 一个带有 while() 语句,两个带有对应的 for()。第一个 for() 仅具有标准 while() 中的停止条件,第二个 for() 也具有迭代器声明和增量。

应用程序 #1 - 来源

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

应用 #2 - 来源

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

应用程序 #3 - 来源

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

如果我们编译所有这些,我们得到:

应用程序 #1 - 字节码

public class While {
  public While();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: iconst_5
       4: if_icmpge     20
       7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: iload_1
      11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      14: iinc          1, 1
      17: goto          2
      20: return
}

应用程序 #2 - 字节码

public class For {
  public For();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: iconst_5
       4: if_icmpge     20
       7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: iload_1
      11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      14: iinc          1, 1
      17: goto          2
      20: return
}

应用程序 #3 - 字节码

public class For2 extends java.lang.Object{
public For2();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   iconst_0
   1:   istore_1
   2:   iload_1
   3:   iconst_5
   4:   if_icmpge       20
   7:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  iload_1
   11:  invokevirtual   #3; //Method java/io/PrintStream.println:(I)V
   14:  iinc    1, 1
   17:  goto    2
   20:  return

}

所以你可以看到,forwhile 的使用没有区别。

【讨论】:

  • 也许您可以将两个 sn-ps 的实际字节码放在您的答案中以证明您的观点:p 这是直观的答案,但这并不意味着它是正确的
  • @Dici 我放了一个样本以便更好地解释。
  • @dgebert +1。 :p
  • 我喜欢这个答案:我认为可以通过将i++ 移动到for 循环中来改进演示:for(; i&lt;5 ; i++){。这样你就可以证明,无论哪种方式,字节码都将iinc放在字节码级别的循环体的末尾,因此反编译器不能总是判断i++是否进入for的最后一个字段循环或作为循环体中的最后一条语句。
  • @mtraceur 而且,更好的是,将初始化语句放在for 的第一部分。
【解决方案2】:

正如其他人已经指出的那样:反编译器(通常)无法区分导致相同字节码的不同源代码。

很遗憾,您没有提供该方法的完整代码。所以下面包含了一些关于这个循环出现在方法中的位置和方式的猜测(这些猜测在某种程度上可能会扭曲结果)。

但是让我们看看这里的一些往返。考虑以下类,其中包含您发布的代码的两个版本的方法:

import java.io.BufferedReader;
import java.io.IOException;
import java.util.regex.Pattern;

public class DecompileExample {

    public static void methodA(BufferedReader reader) throws IOException {
        String line = null;
        int i = 0;
        while ((line = reader.readLine()) != null) {
            System.out.println("line: " + line);
            if (i == 0) {
                String[] colArr = line.split(Pattern.quote("|"));

            } else {
                i++;
            }
        }
    }

    public static void methodB(BufferedReader reader) throws IOException {
        String line = null;
        int i = 0;
        for (String[] colArr = null; (line = reader.readLine()) != null; ++i) {
            System.out.println("line: " + line);
            if (i == 0) {
                colArr = line.split(Pattern.quote("|"));
            } else {
            }
        }
    }
}

编译

javac DecompileExample.java -g:none

将创建相应的类文件。 (注意:-g:none 参数将导致编译器忽略所有调试信息。调试信息可能会被反编译器用于重建原始代码的更逐字版本,特别是,包括原始变量名)

现在查看两种方法的字节码,用

javap -c DecompileExample.class

将产生以下结果:

  public static void methodA(java.io.BufferedReader) throws java.io.IOException;
    Code:
       0: aconst_null
       1: astore_1
       2: iconst_0
       3: istore_2
       4: aload_0
       5: invokevirtual #2                  // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
       8: dup
       9: astore_1
      10: ifnull        61
      13: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: new           #4                  // class java/lang/StringBuilder
      19: dup
      20: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      23: ldc           #6                  // String line:
      25: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      28: aload_1
      29: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      32: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      35: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      38: iload_2
      39: ifne          55
      42: aload_1
      43: ldc           #10                 // String |
      45: invokestatic  #11                 // Method java/util/regex/Pattern.quote:(Ljava/lang/String;)Ljava/lang/String;
      48: invokevirtual #12                 // Method java/lang/String.split:(Ljava/lang/String;)[Ljava/lang/String;
      51: astore_3
      52: goto          4
      55: iinc          2, 1
      58: goto          4
      61: return

  public static void methodB(java.io.BufferedReader) throws java.io.IOException;
    Code:
       0: aconst_null
       1: astore_1
       2: iconst_0
       3: istore_2
       4: aconst_null
       5: astore_3
       6: aload_0
       7: invokevirtual #2                  // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
      10: dup
      11: astore_1
      12: ifnull        60
      15: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      18: new           #4                  // class java/lang/StringBuilder
      21: dup
      22: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      25: ldc           #6                  // String line:
      27: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      30: aload_1
      31: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      34: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      37: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: iload_2
      41: ifne          54
      44: aload_1
      45: ldc           #10                 // String |
      47: invokestatic  #11                 // Method java/util/regex/Pattern.quote:(Ljava/lang/String;)Ljava/lang/String;
      50: invokevirtual #12                 // Method java/lang/String.split:(Ljava/lang/String;)[Ljava/lang/String;
      53: astore_3
      54: iinc          2, 1
      57: goto          6
      60: return
}

一个小区别:String[] colArr = null 被翻译成一个

aconst null
astore_3

在第二个版本的开头。但这是与您在问题中省略的部分代码相关的方面之一。

您没有提及您使用的是哪一个,但来自http://jd.benow.ca/ 的 JD-GUI 反编译器将其反编译为以下内容:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.regex.Pattern;

public class DecompileExample
{
  public static void methodA(BufferedReader paramBufferedReader)
    throws IOException
  {
    String str = null;
    int i = 0;
    while ((str = paramBufferedReader.readLine()) != null)
    {
      System.out.println("line: " + str);
      if (i == 0) {
        String[] arrayOfString = str.split(Pattern.quote("|"));
      } else {
        i++;
      }
    }
  }

  public static void methodB(BufferedReader paramBufferedReader)
    throws IOException
  {
    String str = null;
    int i = 0;
    String[] arrayOfString = null;
    while ((str = paramBufferedReader.readLine()) != null)
    {
      System.out.println("line: " + str);
      if (i == 0) {
        arrayOfString = str.split(Pattern.quote("|"));
      }
      i++;
    }
  }
}

您可以看到这两种情况的代码是相同的(至少关于循环 - 关于我必须引入的“虚拟变量”以编译它,还有一个不同之处,但这与这个问题,可以这么说)。

tl;dr 信息很明确:

不同的源代码可以编译成相同的字节码。因此,相同字节码可以被反编译成不同源代码。但是每个反编译器都必须满足于一个版本的源代码。

(附注:看到没有-g:none进行编译时(即保留调试信息时),我有点惊讶,JD-GUI 甚至设法重建第一个使用@987654332 @-loop 和第二个使用了for-loop。但总的来说,当调试信息被省略时,这根本就不可能了)。

【讨论】:

  • 我正在使用 IntelliJ IDEA,它使用 fernflower 反编译器。感谢您详细的回答。
  • @KumarAnkit 正如 cmets 关于-g:none 标志所指出的,结果不仅取决于反编译器,还取决于首先如何生成.class 文件。 (但我假设 IDE 通常不会省略调试信息 - 最后,它们基本上打算保留它以便在一些不错的调试器 UI 中使用它)
  • +1 我认为您可以通过更明确地强调源代码循环体末尾的 i++ 和 for 循环声明末尾的 ++i映射到字节码循环体末尾的相同指令。
  • @mtraceur 是的,更一般地说:我考虑让初始代码更“明智”,也就是说,实际上 do 使用icolArr 进行一些明智的操作.但是在某些时候,这涉及到如此多的更改,以至于我害怕将发布的代码最初隐藏在这些更改下。不确定最好的解决方案。但如果问题是用可编译的代码更新的,我会相应地更新答案。
【解决方案3】:

这基本上是因为字节码的性质。 Java字节码有点像汇编语言,所以没有forwhile循环这样的东西,只有跳转指令:goto。所以whilefor循环之间可能没有区别,两者都可以编译成相似的代码,反编译器只是猜测。

【讨论】:

  • 虽然如此,但我认为一个好的答案应该对实际的字节码进行分析。我现在没有动力去做,所以我会坚持使用 cmets,但只是说'
【解决方案4】:

for 循环和while 循环代码段都可以翻译成相似的机器码。之后在反编译时,反编译器必须选择two possible 场景之一。

我想这就是这里发生的事情。

简单地说:

compile(A) -> C

compile(B) -> C

所以当你得到C,那么应该有一个猜测选择AB

【讨论】:

  • 也许您可以将两个 sn-ps 的实际字节码放在您的答案中以证明您的观点:p 这是直观的答案,但这并不意味着它是正确的
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-02-02
  • 1970-01-01
  • 2015-12-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多