Strange behaviour of Kotlin compiler or Java decompiler
Asked Answered
K

2

6

This question is driven by my curiosity alone, so I would like to receive a full answer, rather than simple "yes" or "no".

Let's consider this piece of code:

// Is stored in util files and used to omit annoying (this as? Smth)?.doSmth()
inline fun <reified T> Any?.cast(): T? {
    return this as? T
}

class PagingOnScrollListener(var onLoadMore: (currentPage: Int, pageSize: Int) -> Unit) : RecyclerView.OnScrollListener() {

    constructor() : this({ _, _ -> Unit })

    private var loading = false
    private var currentPage = 0
    private var latestPageSize = -1

    var visibleThreshold = VISIBLE_THRESHOLD_DEFAULT
    var pageSize = PAGE_SIZE_DEFAULT

    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        val linearLayoutManager = recyclerView.linearLayoutManager

        val totalItemCount = linearLayoutManager.itemCount
        val lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition()

        if (!loading && totalItemCount - lastVisibleItem <= visibleThreshold
                && latestPageSize !in 0 until pageSize) {
            currentPage++
            loading = true
            onLoadMore(currentPage, pageSize)
        }
    }

    private inline val RecyclerView.linearLayoutManager
        get() = layoutManager?.cast<LinearLayoutManager>()
                ?: throw IllegalStateException("PagingOnScrollListener requires LinearLayoutManager to be attached to RecyclerView!")

    companion object {
        private const val VISIBLE_THRESHOLD_DEFAULT = 4
        private const val PAGE_SIZE_DEFAULT = 10
    }
}

When I use "Show Kotlin Bytecode" tool in AndroidStudio, and then click "Decompile" button, I see this java code (I deleted some irrelevant stuff):

public final class PagingOnScrollListener extends RecyclerView.OnScrollListener {
   private boolean loading;
   private int currentPage;
   private int latestPageSize;
   private int visibleThreshold;
   private int pageSize;
   @NotNull
   private Function2 onLoadMore;
   private static final int VISIBLE_THRESHOLD_DEFAULT = 4;
   private static final int PAGE_SIZE_DEFAULT = 10;

   public PagingOnScrollListener(@NotNull Function2 onLoadMore) {
      Intrinsics.checkParameterIsNotNull(onLoadMore, "onLoadMore");
      super();
      this.onLoadMore = onLoadMore;
      this.latestPageSize = -1;
      this.visibleThreshold = 4;
      this.pageSize = 10;
   }

   public PagingOnScrollListener() {
      this((Function2)null.INSTANCE);
   }

   public void onScrolled(@NotNull RecyclerView recyclerView, int dx, int dy) {
      Intrinsics.checkParameterIsNotNull(recyclerView, "recyclerView");
      super.onScrolled(recyclerView, dx, dy);
      int $i$f$getLinearLayoutManager = false;
      RecyclerView.LayoutManager var10000 = recyclerView.getLayoutManager();
      if (var10000 != null) {
         Object $this$cast$iv$iv = var10000;
         int $i$f$cast = false;
         var10000 = $this$cast$iv$iv;
         if (!($this$cast$iv$iv instanceof LinearLayoutManager)) {
            var10000 = null;
         }

         LinearLayoutManager var10 = (LinearLayoutManager)var10000;
         if (var10 != null) {
            LinearLayoutManager linearLayoutManager = var10;
            int totalItemCount = linearLayoutManager.getItemCount();
            int lastVisibleItem = linearLayoutManager.findLastVisibleItemPosition();
            if (!this.loading && totalItemCount - lastVisibleItem <= this.visibleThreshold) {
               int var11 = this.pageSize;
               int var12 = this.latestPageSize;
               if (0 <= var12) {
                  if (var11 > var12) {
                     return;
                  }
               }

               int var10001 = this.currentPage++;
               this.loading = true;
               this.onLoadMore.invoke(this.currentPage, this.pageSize);
            }

            return;
         }
      }

      throw (Throwable)(new IllegalStateException("EndlessOnScrollListener requires LinearLayoutManager to be attached to RecyclerView!"));
   }
}

Here we can see some strange code:

1.

// in constructor:
Intrinsics.checkParameterIsNotNull(onLoadMore, "onLoadMore");
super();

Java requires super call to be the first statement in the constructor body.


2.

this((Function2)null.INSTANCE); which corresponds to constructor() : this({ _, _ -> Unit }) What null.INSTANCEmeans? Why is there no anonymous object as was intended?

this(new Function2() {
  @Override
  public Object invoke(Object o1, Object o2) {
    return kotlin.Unit.INSTANCE;
  }
});

3.

No @Override annotation on method onScrolled. Was it too hard to add an annotation to the method with override modifier? However the @NonNull and @Nullable annotations are present.


4.

int $i$f$getLinearLayoutManager = false;

Boolean value is being assigned to int variable? Why this line is present here? This variable has no usage. Why it declares a variable that isn't going to be used?


5.

RecyclerView.LayoutManager var10000 = recyclerView.getLayoutManager();
if (var10000 != null) {
  Object $this$cast$iv$iv = var10000; // what's the purpose of this assignment?
  int $i$f$cast = false;
  var10000 = $this$cast$iv$iv; // Incompatible types. RecyclerView.LayoutManager was expected but got Object.
  ...

6.

if (!this.loading && totalItemCount - lastVisibleItem <= this.visibleThreshold) {
  int var11 = this.pageSize;
  int var12 = this.latestPageSize;
  if (0 <= var12) {
    if (var11 > var12) {
      return;
    }
  }
  ...
}

Why don't make it simpler with this?

if (!this.loading && totalItemCount - lastVisibleItem <= this.visibleThreshold && (0 > this.latestPageSize || this.pageSize < this.latestPageSize)) 

7.

// Unhandled exception: java.lang.Throwable.
throw (Throwable)(new IllegalStateException("EndlessOnScrollListener requires LinearLayoutManager to be attached to RecyclerView!"));

Why it casts IllegalStateException to Throwable if we know that IllegalStateException extends Throwable? What's the purpose?


Is that really the code that is being performed in production or just Java Decompiler can't figure out all that stuff?

Kevel answered 5/10, 2019 at 18:39 Comment(1)
4. the unused, boolean-like ints are tricks used by the debugger : discuss.kotlinlang.org/t/…Sorbian
T
6

Most of your questions can be answered as Java != Java bytecode. Compilation removes a lot of information from Java that is only required at compile time, and the bytecode format also supports a lot of things that aren't valid at the Java level.

To answer your specific questions:

  1. Java requires this, but Java bytecode has no such restriction. Presumably, Kotlin's knowledge that the parameter shouldn't be null resulted in the compiler inserting code to check this at runtime. Since bytecode freely allows code prior to the super constructor call (with some caveats about accessing uninitialized objects), there is no problem until you try to decompile it.

  2. That looks like Kotlin specific functionality, so I'm not sure.

  3. Some annotations are preserved in the bytecode and some aren't. @Override has no runtime behavior and is only useful as a compile time check, so it makes sense that it would be set to compile time only.

  4. At the bytecode level, there is no such thing as booleans (apart from method signatures). All boolean (and char and short and byte) local variables are compiled to ints, with false = 0 and true = 1. This means that the decompiler has to guess whether any given variable was meant to be an int or a boolean, which is a very difficult task that is impossible to always get right.

  5. Presumably the decompiler got confused, or the bytecode was such that it is difficult to decompile into valid Java. Remember that Java bytecode has much looser type checking than Java, and a lot of compile time information disappears after compilation, so it's not straightforward to decompile bytecode into valid Java.

  6. Because the decompiler wasn't programmed to do that simplification? You can try asking the decompiler authors to add that, but it's a lot harder than you might think.

  7. It's impossible to know for sure without looking at the bytecode, but the Throwable cast was likely added by the decompiler. Remember, bytecode and Java source are incompatible formats and decompilation is not an exact transformation.

Is that really the code that is being performed in production or just Java Decompiler can't figure out all that stuff?

If you are interested in this topic, I would highly, highly recommend learning how Java bytecode works, and then use a Java bytecode disassembler to see what is actually going on under the hood. This will allow you to see what is in the bytecode and what might be artifacts of decompilation.

Toneytong answered 5/10, 2019 at 19:55 Comment(1)
Thanks a lot for an answer, but I'm not fully convinced. The main question "why?" is not answered for 4th and 7th. I understand that there is no such thing as booleans at the bytecode, but why does it create this variable anyway? It isn't going to be used. And why it casts IllegalStateException to Throwable?Kevel
T
3

There are 2 proven ways to find out what the bytecode does: run it and read it.

If you run it, you will see that everything works as it is written in Kotlin.

Now let me read bytecode and explain it.

1. Bytecode doesn't care about java requirements. Some actions can be performed before super() call even in java. For example, here super(string + string); addition is performed before it.


2. Bytecode:

GETSTATIC me/stackoverflow/a10/PagingOnScrollListener$1.INSTANCE : Lme/stackoverflow/a10/PagingOnScrollListener$1;
CHECKCAST kotlin/jvm/functions/Function2
INVOKESPECIAL me/stackoverflow/a10/PagingOnScrollListener.<init> (Lkotlin/jvm/functions/Function2;)V

My translation of this in Java:

this((Function2)PagingOnScrollListener$1.INSTANCE);

I think java decompiler fails to decompile it correctly because of a weird class name 1. Java uses numbers for anonymous class names, but these classes cannot have static declaration.

There is no new function instance creation here because Kotlin is smart enough to see that the same instance can be used every time.


3. @Override annotation is annotated with @Retention(RetentionPolicy.SOURCE), so it is removed from the bytecode.


4. Bytecode:

ICONST_0
ISTORE 7

My translation of this in Java:

int i7 = 0;

Java decompiler failed to decompile it correctly because there are no boolean local variables in java bytecode, they are replaced with int variables.


5. The bytecode created here by Kotlin is very complex. The Java decompiler must have been unable to decompile it correctly as well as me.


6. The decompiler currently doesn't support this simplification.


7. There is this cast in bytecode:

CHECKCAST java/lang/Throwable
Thyroid answered 5/10, 2019 at 21:9 Comment(1)
Thanks for your response and for digging the bytecode (The answer for 2nd option was great and I got it). But your other answers aren't answering the question "Why". Maybe I should mention it in the question, so I will edit it. In particular in 4th option I see that bytecode but I don't understand why is it there in the first place. Why it declares variable that isn't going to be used? And in 7th what's the purpose of this line (CHECKCAST java/lang/Throwable) if we know that IllegalStateException is instance of Throwable?Kevel

© 2022 - 2024 — McMap. All rights reserved.