0

I'm working on some android project where I'm mixing Java and Kotlin. I have a piece of Kotlin code that I decompiled in Java to see how it is actually converted.

Kotlin code

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

Android studio created Java equivalent

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);
}

What bothers me is this CoockieJsonRequest var10000 = new CoockieJsonRequest;. So, basically, here we can see that the code allocated the memory to CoockieJsonRequest with new operator, but don't call it constructor(no braces). Instead of this, the code does some other operations(unwrapping pushModel object) and only then initializes CoockieJsonRequest using JVM <init>. This looks really weird to me since I always thought that the object has to be constructed when it is allocated.

So, my questions - it is how it works (construction can be deferred) or something is wrong with Android Studion Kotlin decompiler and it just creates weird decompile outputs?

Son Truong
  • 13,661
  • 5
  • 32
  • 58
starwarrior8809
  • 361
  • 1
  • 10
  • 2
    The Java equivalent that you're looking at is decompiled from JVM bytecode. At the bytecode level it is possible to do what that code does (see e.g. https://stackoverflow.com/a/53223409/1524450). Whether it is allowed in Java code is a different matter. – Michael Dec 08 '20 at 11:24

2 Answers2

3

TL;DR: It's nearly a normal instance construction, but the conditionals confuse the decompiler.

At bytecode level, it's two different instructions. new creates an uninitialized instance of the given class. invokespecial, referencing the <init> method, initializes the instance using the given constructor. By no means is it obligatory that they immediately follow one another.

The code you're seeing roughly matches the following java code (after inlining the synthetic variables):

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

It seems to be quite normal that Java compilers

  • first create the instance with the new instruction,
  • then prepare the arguments for the constructor,
  • and finally call the constructor <init> method using the invokespecial instruction.

So, with a Java compiler, the Java instance creation from above will probably produce a very similar bytecode sequence, hence a similar decompilation.

Only the Intrinsics null checks make a difference in the bytecode, and probably confuse the decompiler enough so that it couldn't inline the argument expressions into a "normal" constructor invocation.

E.g. when trying to inline the var10004 expression, the if construct could at best be replaced with a ternary operator, making the resulting Java code at least complicated, if not impossible. So, it's quite plausible that the decompiler fails here.

Ralf Kleberhoff
  • 6,990
  • 1
  • 13
  • 7
  • Thank you for the reply, it explains a lot of things ! So, basically in this particular case code looks weird because Kotlin spesific `Intrinsics ` confused decompiler . But in general allocation and initializatoin are not guaranteed to follow immediately one after another ? Did I got you correct ? – starwarrior8809 Dec 08 '20 at 12:49
  • 1
    It’s possible to encounter an equivalent of that `Intrinsics.checkExpressionValueIsNotNull(…)` with Java code as well. E.g. when using `String s = "foo"; new Thread(s::toString);`, there will be an invocation of `java.util.Objects.requireNonNull(…)` between the instantiation of `Thread` and the invocation of `Thread.(…)`. – Holger Dec 08 '20 at 13:14
  • @starwarrior8809 Yes, at bytecode level, allocation and initialization can be separated by other instructions. – Ralf Kleberhoff Dec 08 '20 at 14:38
2

At the bytecode level, arbitrary code may be placed between the instantiation of the object and the invocation of the constructor, as long as the constraints, as described in this answer are obeyed.

Generally, an expression of the form new Type(expression) gets compiled to an instantiation of Type, followed by the code for expression, followed by an invocation of the constructor, passing the results of the preceding evaluation of the expression.

But on the bytecode level, there is no distinction between expressions and statements. Even on the source level, the distinction is blurring. For example, with Java 14 you could use the following construct to have statements inside an expression:

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()));
}

which compiles to

  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

Note how the instance created in instruction at offset #0 gets initialized at offset #52 with quite complex instructions in-between.

Holger
  • 285,553
  • 42
  • 434
  • 765