Java: 对象 construction/initialization 可以延迟吗?

Java: object construction/initialization can be deferred?

我正在做一些 android 项目,我正在混合 Java 和 Kotlin。我在 Java 中反编译了一段 Kotlin 代码,以查看它实际上是如何转换的。

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 <init> 初始化 CoockieJsonRequest。这对我来说真的很奇怪,因为我一直认为对象必须在分配时构造。

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

TL;DR: 这几乎是一个正常的实例构造,但是条件语句混淆了反编译器。

在字节码级别,这是两条不同的指令。 new 创建给定 class 的未初始化实例。 invokespecial,引用 <init> 方法,使用给定的构造函数初始化实例。绝不是强制性的,他们必须立即相互关注。

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

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

Java编译器

好像很正常
  • 先用new指令创建实例,
  • 然后准备构造函数的参数,
  • 最后使用invokespecial指令调用构造函数<init>方法。

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

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

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

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

通常,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 处初始化的,中间有相当复杂的指令。