【问题标题】:Java: object construction/initialization can be deferred?Java:对象构造/初始化可以推迟吗?
【发布时间】:2020-12-08 11:13:52
【问题描述】:

我正在开发一些混合 Java 和 Kotlin 的 android 项目。我有一段 Kotlin 代码,我用 Java 反编译了它,看看它是如何实际转换的。

Kotlin 代码

fun postSettingToServer() {
  val request = CoockieJsonRequest(Request.Method.POST, URLBuilder.GetPushSettings(this), pushModel!!.toJSON(), null, null)
  VolleySingleton.getInstance(applicationContext).addToRequestQueue(request)
}

Android 工作室创建了 Java 等价物

public final void postSettingToServer() {
    CoockieJsonRequest var10000 = new CoockieJsonRequest;
    String var10003 = URLBuilder.GetPushSettings((Context)this);
    Intrinsics.checkExpressionValueIsNotNull(var10003, "URLBuilder.GetPushSettings(this)");
    PushSettings var10004 = this.pushModel;
    if (var10004 == null) {
      Intrinsics.throwNpe();
    }

    var10000.<init>(1, var10003, var10004.toJSON(), (Listener)null, (ErrorListener)null);
    CoockieJsonRequest request = var10000;
    VolleySingleton.getInstance(this.getApplicationContext()).addToRequestQueue((Request)request);
}

困扰我的是这个CoockieJsonRequest var10000 = new CoockieJsonRequest;。因此,基本上,在这里我们可以看到代码使用new 运算符将内存分配给CoockieJsonRequest,但不要将其称为构造函数(没有大括号)。取而代之的是,代码执行了一些其他操作(解包 pushModel 对象),然后才使用 JVM &lt;init&gt; 初始化 CoockieJsonRequest。这对我来说看起来很奇怪,因为我一直认为必须在分配对象时构造它。

所以,我的问题 - 它是如何工作的(可以推迟构建)或者 Android Studion Kotlin 反编译器有问题,它只会产生奇怪的反编译输出?

【问题讨论】:

  • 您正在查看的 Java 等效项是从 JVM 字节码反编译的。在字节码级别,可以执行该代码所做的事情(参见例如stackoverflow.com/a/53223409/1524450)。 Java代码中是否允许是另一回事。

标签: java android kotlin jvm decompiling


【解决方案1】:

TL;DR:这几乎是一个普通的实例构造,但条件语句让反编译器感到困惑。

在字节码级别,这是两条不同的指令。 new 创建给定类的未初始化实例。 invokespecial,引用 &lt;init&gt; 方法,使用给定的构造函数初始化实例。绝不是强制他们立即相互跟随。

您看到的代码与以下 java 代码大致匹配(在内联合成变量之后):

CoockieJsonRequest request = new CoockieJsonRequest(1, 
    URLBuilder.GetPushSettings((Context)this), 
    this.pushModel.toJSON(), 
    (Listener) null, 
    (ErrorListener) null);

Java 编译器似乎很正常

  • 首先使用new 指令创建实例,
  • 然后为构造函数准备参数,
  • 最后使用invokespecial指令调用构造函数&lt;init&gt;方法。

因此,使用 Java 编译器,从上面创建的 Java 实例可能会产生非常相似的字节码序列,因此会产生类似的反编译。

只有 Intrinsics 空值检查会对字节码产生影响,并且可能会使反编译器感到困惑,以至于它无法将参数表达式内联到“正常”的构造函数调用中。

例如当尝试内联var10004 表达式时,if 构造最多可以替换为三元运算符,这使得生成的Java 代码至少复杂,如果不是不可能的话。所以,反编译器在这里失败是很合理的。

【讨论】:

  • 谢谢你的回复,它解释了很多东西!所以,基本上在这种特殊情况下,代码看起来很奇怪,因为 Kotlin 特殊 Intrinsics 混淆了反编译器。但是一般分配和初始化不保证紧接着一个接一个?我说对了吗?
  • 在 Java 代码中也可能遇到 Intrinsics.checkExpressionValueIsNotNull(…) 的等价物。例如。使用String s = "foo"; new Thread(s::toString);时,在Thread的实例化和Thread.&lt;init&gt;(…)的调用之间会有java.util.Objects.requireNonNull(…)的调用。
  • @starwarrior8809 是的,在字节码级别,分配和初始化可以通过其他指令分开。
【解决方案2】:

在字节码级别,可以在对象的实例化和构造函数的调用之间放置任意代码,只要遵守this answer 中描述的约束即可。

通常,new Type(expression) 形式的表达式被编译为 Type 的实例化,然后是 expression 的代码,然后是构造函数的调用,传递前面对表达式求值的结果.

但在字节码层面,表达式和语句没有区别。即使在源代码层面,区别也很模糊。例如,对于 Java 14,您可以使用以下构造在表达式中包含语句:

public static void main(String[] args) {
    new String(switch(0) {
        default:
            try {
                yield Files.readAllBytes(Path.of(""));
            }
            catch(IOException ex) {
                yield ex.toString().getBytes();
            }
    });

    showBytecode();
}

private static void showBytecode() {
    ToolProvider.findFirst("javap")
        .ifPresent(tp -> tp.run(System.out, System.err, "-c", Tmp.class.getName()));
}

编译成

  public static void main(java.lang.String[]);
    Code:
       0: new           #1        // class java/lang/String
       3: dup
       4: astore_1
       5: astore_2
       6: iconst_0
       7: lookupswitch  { // 0
               default: 16
          }
      16: ldc           #3        // String
      18: iconst_0
      19: anewarray     #1        // class java/lang/String
      22: invokestatic  #5        // InterfaceMethod java/nio/file/Path.of:(Ljava/lang/String;[Ljava/lang/String;)Ljava/nio/file/Path;
      25: invokestatic  #11       // Method java/nio/file/Files.readAllBytes:(Ljava/nio/file/Path;)[B
      28: astore_3
      29: aload_2
      30: aload_1
      31: aload_3
      32: goto          52
      35: astore        4
      37: aload         4
      39: invokevirtual #19       // Method java/io/IOException.toString:()Ljava/lang/String;
      42: invokevirtual #23       // Method java/lang/String.getBytes:()[B
      45: astore_3
      46: aload_2
      47: aload_1
      48: aload_3
      49: goto          52
      52: invokespecial #27       // Method java/lang/String."<init>":([B)V
      55: pop
      56: invokestatic  #31       // Method showBytecode:()V
      59: return
    Exception table:
       from    to  target type
          16    29    35   Class java/io/IOException

请注意在偏移量 #0 的指令中创建的实例是如何在偏移量 #52 处初始化的,其间有相当复杂的指令。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-25
    • 2011-03-10
    • 2015-01-03
    • 2015-07-03
    • 1970-01-01
    • 2011-03-09
    相关资源
    最近更新 更多