Странное поведение компилятора Kotlin или декомпилятора Java - PullRequest
3 голосов
/ 05 октября 2019

Этот вопрос вызван только моим любопытством, поэтому я хотел бы получить полный ответ, а не простое «да» или «нет».

Давайте рассмотрим этот фрагмент кода:

// 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
    }
}

Когда я использую инструмент «Показать Kotlin Bytecode» в AndroidStudio, а затем нажимаю кнопку «Декомпилировать», я вижу этот код Java (я удалил некоторые ненужные вещи):

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

Здесь мы можемсм. некоторый странный код:

1.

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

Java требует, чтобы super вызывал первый оператор в теле конструктора.


2.

this((Function2)null.INSTANCE);, что соответствует constructor() : this({ _, _ -> Unit }) Что означает null.INSTANCE? Почему нет анонимного объекта, как это было задумано?

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

3.

Нет @Override аннотация для метода onScrolled. Было ли слишком сложно добавить аннотацию к методу с модификатором override? Однако аннотации @NonNull и @Nullable присутствуют.


4.

int $i$f$getLinearLayoutManager = false;

Boolean присваивается intпеременная? Почему эта строка присутствует здесь? Эта переменная не используется. Почему он объявляет переменную, которая не будет использоваться?


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

Почему бы не сделать это проще?

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!"));

ПочемуIllegalStateException к Throwable, если мы знаем, что IllegalStateException extends Throwable? Какова цель?


Это действительно код, который выполняется в производстве, или просто Java Decompiler не может разобраться во всем этом?

Ответы [ 2 ]

4 голосов
/ 05 октября 2019

На большинство ваших вопросов можно ответить как Java! = Java bytecode. Компиляция удаляет из Java много информации, которая требуется только во время компиляции, а формат байт-кода также поддерживает множество вещей, которые недопустимы на уровне Java.

Чтобы ответить на ваши конкретные вопросы:

  1. Java требует этого, но байт-код Java не имеет таких ограничений. Предположительно, знание Kotlin о том, что параметр не должен быть нулевым, привело к тому, что компилятор вставил код, чтобы проверить это во время выполнения. Поскольку байт-код свободно разрешает код до вызова супер-конструктора (с некоторыми оговорками относительно доступа к неинициализированным объектам), проблем не будет, пока вы не попытаетесь декомпилировать его.

  2. Это похоже на специфическую функциональность Kotlin, поэтому я не уверен.

  3. Некоторые аннотации сохраняются в байт-коде, а некоторые - нет,@Override не имеет поведения во время выполнения и полезен только для проверки времени компиляции, поэтому имеет смысл установить его только на время компиляции.

  4. На уровне байт-кода существуетнет такого понятия, как логическое значение (кроме сигнатур методов). Все логические (и char, и short, и байты) локальные переменные скомпилированы в целые числа с false = 0 и true = 1. Это означает, что декомпилятор должен угадать, должна ли какая-либо данная переменная быть int или логической, чтоочень сложная задача, которую невозможно всегда получить правильно.

  5. Предположительно, декомпилятор запутался, или байт-код был таков, что его трудно декомпилировать в действительный Java. Помните, что байт-код Java имеет гораздо более слабую проверку типов, чем Java, и большая часть информации времени компиляции исчезает после компиляции, поэтому декомпилировать байт-код в допустимую Java непросто.

  6. Поскольку декомпилятор былне запрограммированы на такое упрощение? Вы можете попытаться попросить авторов декомпилятора добавить это, но это намного сложнее, чем вы думаете.

  7. Невозможно узнать наверняка, не глядя на байт-код, но приведение типа Throwable былоскорее всего добавлено декомпилятором. Помните, что байт-код и исходный код Java являются несовместимыми форматами, и декомпиляция не является точным преобразованием.

Это действительно код, который выполняется в производстве, или просто Java Decompiler не может разобраться во всем этом?

Если выЕсли вы заинтересованы в этой теме, я очень рекомендую узнать, как работает байт-код Java, а затем использовать дизассемблер байт-кода Java , чтобы увидеть, что на самом деле происходит под капотом. Это позволит вам увидеть, что находится в байт-коде и какие могут быть артефакты декомпиляции.

2 голосов
/ 06 октября 2019

Есть 2 проверенных способа выяснить, что делает байт-код: запустите его и прочитайте его.

Если вы запустите его, вы увидите, что все работает так, как написано в Kotlin.

Теперь позвольте мне прочитать байт-код и объяснить его.

1. Байт-код не заботится о требованиях Java. Некоторые действия могут быть выполнены до super() вызова даже в Java. Например, здесь super(string + string); добавление выполняется перед ним.


2. Байт-код:

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

Мой перевод этого на Java:

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

Я думаю, что Java-декомпилятор не может правильно декомпилировать его из-за странного имени класса 1. Java использует числа для имен анонимных классов, но эти классы не могут иметь статического объявления.

Здесь не создается новый экземпляр функции, потому что Kotlin достаточно умен, чтобы видеть, что один и тот же экземпляр может использоваться каждый раз.


3. @Override аннотация помечена @Retention(RetentionPolicy.SOURCE), поэтому она удаляется из байт-кода.


4. Байт-код:

ICONST_0
ISTORE 7

Мой перевод этого на Java:

int i7 = 0;

Java-декомпилятору не удалось правильно декомпилировать его, поскольку в байт-коде java нет boolean локальных переменных, они замененыс int переменными.


5. Байт-код, созданный здесь Kotlin, очень сложен. Декомпилятор Java, должно быть, не смог правильно декомпилировать его так же, как и я.


6. Декомпилятор в настоящее время не поддерживает это упрощение.


7. Есть этот бросок в байт-коде:

CHECKCAST java/lang/Throwable
...