Kotlin Decompiled: знакомимся с языком

Всем привет, меня зовут Клименко Руслан. Сейчас я ведущий разработчик програмного обеспечения в одной из украинских аутсорсинговых компаний, part-time архитектор и создатель образовательного проекта Dobroe IT.

Эта статья будет посвящена языку Kotlin, да и что уж там, платформе JVM в целом, поскольку команда Kotlin имеет ну уж очень амбициозные планы и видит свой язык одним из флагманов платформы. Я с таким мнением согласен и попытаюсь рассказать вам почему.

Главная цель статьи — познакомить в первую очередь Java-разработчиков с Kotlin, показать, каким образом этот язык может упростить работу инженера, победить рутину и сделать программирование под JVM весёлым опять. Ну, или около того.

Бытует мнение, что Kotlin — это нишевый язык, который заточен лишь на работу в экосистеме Android. Тем не менее мой опыт говорит мне о том, что это не совсем так.

На данный момент я реализовал около 10 enterprise-проектов с использованием этого языка, и во всех случаях он был незаменимым помощником. Оборачиваясь назад, хочу сказать, что сейчас у меня нет никаких стимулов возвращаться к использованию pure Java для enterprise-приложений. Более того, JetBrains (да-да, те самые ребята, которые создали лучшую IDE для Java-разработчиков, также являются создателями Kotlin) внедряют язык не только в мир JVM, но и в мир браузерных приложений (существует родной компилятор Kotlin в JavaScript), и даже в native код (проект Kotlin Native).

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

Как мы будем знакомиться с Kotlin?

В качестве метода исследования мы будем использовать декомпиляцию ПО, написанного на Kotlin, в код на Java. Такое преобразование возможно в силу того, что Kotlin, так же, как в свою очередь и Java, Scala, Groovy и многие другие языки, компилируются в bytecode виртуальной машины Java (JVM bytecode).

То есть все эти языки позволяют создавать программы, работающие на JVM, и, соответственно, в скомпилированном виде такие программы будут выглядеть приблизительно одинаково. Следовательно, декомпилируя bytecode программы, изначально написанной на Kotlin, мы увидим то, как бы мы должны были писать на Java, чтобы получить такой же результат.

Пошагово процесс декомпиляции выглядит следующим образом:

  1. Пишем программку на Kotlin.
  2. Компилируем её с помощью компилятора kotlinc.
  3. Декомпилируем приложение в Java-код с помощью стандартного декомпилятора IntelliJ IDEA (конечно же, любой другой адекватный инструмент также подойдет).

Самая простая программа на Kotlin и ее декомпиляция

Традиционно первое приложение, которое мы напишем на новом языке, будет: «Привет, мир!». Открываем свою любимую IDE, создаем файл Test.kt и пишем следующий код:

fun main(args: Array<String>) {
    print("Hello world")
}

Что мы только что сделали? Мы создали метод main, который принимает массив строк (да-да, тот самый, родной). В глаза сразу бросаются некоторые отличия от Java. К примеру, при определении метода нужно теперь использовать ‘fun’, а вот ‘return type’ для ‘void’ методов указывать не обязательно. Параметры метода также объявляются чуть по-иному: вначале определяем имя, потом — тип.

По сути, этого кода вполне достаточно, и теперь мы можем смело воспользоваться компилятором kotlinc (скачать и установить его можно отсюда). В результате мы увидим файл с именем TestKt.class.

Далее, если вы используете IntelliJ IDEA — открываем окно Kotlin Bytecode и видим привычный для всех Java-разработчиков набор мнемоник.

Увеличить

До нашей заветной цели — декомпиляции программы — остался последний шаг. Нужно нажать на кнопочку «Decompile» в левом верхнем углу окна Kotlin Bytecode. Нажимаем и... Вуаля! В главном окне мы видим следующее:

Увеличить

Только что мы написали программу на Kotlin, скомпиллировали её, а после — превратили в программу на Java, используя декомпилятор.

Что мы видим в декомпилированном коде

Давайте разберем всё по порядку:

1. Исходный код программы на Kotlin содержал всего один метод — main, который был объявлен на уровне файла. Как мы знаем, в Java так делать нельзя. Все методы, включая метод main, должны быть объявлены в классе. Kotlin же упрощает жизнь программистов и позволяет писать методы в различных контекстах — на уровне файла, класса или другого метода. А вот для совместимости с виртуальной машиной Java Kotlin обязан добавлять методы, которые просто лежат в файле в класс. И поэтому в таком случае автоматически создается класс с именем файла + Kt (в нашем случаем мы видим класс TestKt).

Внутри класса мы видим вполне ожидаемый метод main с соответствующей сигнатурой. На что стоит обратить внимание: параметр метода помечен аннотацией @NotNull (зачем это нужно, можно узнать вот тут).

На самом деле в Kotlin довольно жесткая система типов. Существует две категории: те, которые могут принимать значение null (nullable типы), и те, которые не могут содержать null (not nullable).

По умолчанию все типы в Kotlin not nullable. Если вы всё же любите экстрим и хотите рискнуть, создать nullable тип — то вам прийдется явно об этом сказать компилятору с помощью символа ?. К примеру:

String — not nullable,
String? — nullable.

В методе main есть еще одна инструкция, которая связана с системой типов в Kotlin:

Intrinsics.checkParameterIsNotNull(args, "args");

Именно в этой строчке и происходит актуальная проверка параметра (args в нашей программе объявлен как not nullable). Если args всё же null, то checkParameterIsNotNull выкинет исключение.

Хорошая новость состоит в том, что внутри Kotlin-приложения все проверки типов происходят еще на этапе компиляции. Все not nullable переменные будут считаться безопасными, а при работе с nullable переменными компилятор будет просить указать явно, что делать в случае, если значение равно null.

Однако при разработке приложения, использующего другие JVM-языки, kotlinc сам по себе не может понять — может ли вернуть метод, написанный на другом языке null, или нет. И для решения этой проблемы можно использовать следующие правила:

  • Если вы имеете возможность модифицировать код на другом языке — используйте аннотации @NotNull и @Nullable, которые подскажут компилятору Kotlin как воспринимать возвращаемый тип.
  • Если у вас нет полномочий или возможности модифицировать код на другом языке — воспринимайте все типы как nullable по умолчанию и делайте все соответствующие проверки на начальном этапе выполнения приложения.

Второй подход хоть и является более энергозатратным, но все же более предпочтителен в силу того, что, вероятно, вам прийдется столкнуться не только с JVM-based языками, а и с другими сторонними системами (databases, message brokers, web services etc.), а они в свою очередь также могут возвращать null-значения.

3. Аннотация метадата. Зачем она нужна? Короткий ответ: для того, чтобы отобразить в рантайме, который будет использовать всю ту же старую добрую JVM, те нюансы, которые есть в Kotlin, но не существуют в Java (к примеру, разницу между mutable vs immutable коллекциями).

На примере ‘Hello world’ мы прикоснулись к Kotlin и даже посмотрели, что именно он делает за нас для того, чтобы мы могли быстро писать код. На самом деле пока что мы не видели ничего удивительного, а лишь познакомились с методом декомпиляции, который каждый может использовать самостоятельно для того, чтобы изучать JVM-языки.

Еще несколько примеров программ на Kotlin

Допустим, мы хотим добавить метод в уже существующий тип. Очень хорошо, если оригинальный класс находится под нашим контролем и не ограничен никакими обязательствами. Но что если этот класс финальный? Как мы можем добавить к нему поведение, не прибегая к грязным трюкам, таким как создание wrapper-класса или манипуляции с байткодом? На Java эта задача выглядит не самой простой, а вот Kotlin вполне сможет нам помочь.

‘Extension methods’ позволяют добавлять методы для типов вне их контекста. Ниже представлен пример метода, который может быть реализован на уровне файла. Этот метод расширяет класс String, добавляя метод encode (caesar encryption).

fun String.encode(shift: Int) =
        String(this.map { it + shift }.toCharArray())

Интересно, что методы, имеющие всего одну инструкцию, могут быть реализованы с помощью оператора присваивания (идея не новая, но человеку, работающему только с Java, это может показаться интересным). Возвращаемый тип в таком случае указывать явно также не нужно. Компилятор достаточно умный, чтобы понять, что к чему самостоятельно. После объявления метода encode мы готовы его вызывать:

fun main(args: Array<String>) {
    print("test".encode(3))
}

Как же работают Extension methods? Ведь мы с вами знаем, что наследовать финальные типы нельзя (а String, как известно, является final).

Давайте посмотрим на декомпилированное приложение:

Увеличить

Теперь всё становится на свои места. Конечно же, никакого наследования здесь нет. Метод encode не объявлен в классе String или его подтипе. Это обычный статический helper-метод, который лишь принимает строку в качестве параметра, а при вызове метода данная строка в него и передается.

Очень удобно, что в Kotlin мы можем делать подобные вещи всего в одну строчку.

Предлагаю рассмотреть ещё один, на этот раз последний, пример того, как Kotlin упрощает жизнь разработчикам.

Все мы не раз писали классы, которые содержат лишь состояние (JPA Entities, Domain models, DTOs и многие другие). И, конечно же, нам очень не хотелось писать к ним те самые канонические toString(), equals() и hashCode(), а также вечные getters-setters. Некоторые из нас даже пытались использовать Lombok, и это очень хорошо. Кто-то устал и отправился в мир абстрактных вычислений Scala, и это нормально (хотя могло быть и лучше). Но сейчас я хочу показать, что по этому поводу думает Kotlin. Предлагаю рассмотреть следующий пример:

data class Test(val a: String = "", val b: Int = 0)

Data classes. При добавлении всего одного ключевого слова ‘data’ перед объявлением класса на выхлопе мы получаем следующую программу:

public final class Test {
   @NotNull
   private final String a;
   private final int b;
   @NotNull
   public final String getA() {
      return this.a;
   }
   public final int getB() {
      return this.b;
   }
   public Test(@NotNull String a, int b) {
      Intrinsics.checkParameterIsNotNull(a, "a");
      super();
      this.a = a;
      this.b = b;
   }
   // $FF: synthetic method
   public Test(String var1, int var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 1) != 0) {
         var1 = "";
      }
      if ((var3 & 2) != 0) {
         var2 = 0;
      }
      this(var1, var2);
   }
   public Test() {
      this((String)null, 0, 3, (DefaultConstructorMarker)null);
   }
   @NotNull
   public final String component1() {
      return this.a;
   }
   public final int component2() {
      return this.b;
   }
   @NotNull
   public final Test copy(@NotNull String a, int b) {
      Intrinsics.checkParameterIsNotNull(a, "a");
      return new Test(a, b);
   }
   // $FF: synthetic method
   // $FF: bridge method
   @NotNull
   public static Test copy$default(Test var0, String var1, int var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = var0.a;
      }
      if ((var3 & 2) != 0) {
         var2 = var0.b;
      }
      return var0.copy(var1, var2);
   }
   public String toString() {
      return "Test(a=" + this.a + ", b=" + this.b + ")";
   }
   public int hashCode() {
      return (this.a != null ? this.a.hashCode() : 0) * 31 + this.b;
   }
   public boolean equals(Object var1) {
      if (this != var1) {
         if (var1 instanceof Test) {
            Test var2 = (Test)var1;
            if (Intrinsics.areEqual(this.a, var2.a) && this.b == var2.b) {
               return true;
            }
         }
         return false;
      } else {
         return true;
      }
   }
}

Круто, правда? :) Кроме озвученных getters-setters, toString(), equals(), hashCode(), мы также получаем перегруженный конструктор, метод copy, выполняющий поверхностную копию над объектом и методы componentX(), которые используются при деструктуризации.

Итоги и советы

На самом деле Kotlin таит в себе ещё много секретиков, но нам пора закругляться. Я хочу спать, да и лонгриды никто не любит. Я надеюсь, что этой статьей я заинтересовал вас (особенно если вы java-разработчик). Ну а дальше...

Вы можете самостоятельно разобраться и освоить свежий, новый, перспективный язык. В этом может помочь:

0. Мой толк на JEE Conf. По сути, эта статья — краткий его пересказ.

1. Книжка «Kotlin in Action».

2. Задачки Kotlin Koans.

3. Документашка.

А еще мы в Dobroe IT планируем сделать курс по Kotlin этой осенью. Поэтому если вы молоды, горячи и очень хотите, то можно вступить в нашу группу и смиренно ждать анонса курса.

На этом сегодня всё. Спасибо за внимание. Будьте счастливы.

LinkedIn

9 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

1. Почему котлин отстой sohabr.net/habr/post/322256
2. Как про***ать миграциою с Java на Котлин
blog.usejournal.com/...​-android-app-325b57c9ddbb

В первой статье адекватных аргументов против практически нет. В некоторых моментах автор просто не захотел разбираться.
Вторая статья о том что котлин не работает вместе с lombok (точнее, работает с костылями).
Да, ужасный язык.

Якраз в першій статті автор розібрався і описав достатньо багато ньюансів. Звісно, це актуально, якщо використовувати котлін трохи дальше, ніж формошльопство на андроіді.

Якраз в першій статті автор розібрався і описав достатньо багато ньюансів.

Из пункта про null-safety мне ясно что он не разобрался с делегированием полей с помощью lazy/notNull. Если бы я использовал !! и ? как в его примерах, то возможно тоже бы писал статьи в стиле «котлин — отстой».

Литералы для коллекций (кстати, в примере с инициализацией словаря, функция to сделала бы код чище) и обход стирания типов — были бы хорошими фичами, но их отсутствие не делает язык отстойным.

Остальные пункты это вообще жесткое имхо основанное на привычке, особенно про тернарный оператор и присваивание как выражение.

Я согласен с некоторыми пунктами, но стиль изложения просто ужас, а содержание явно не соответствует названию, я бы переименовал в «почему мне не понравился котлин».

Звісно, це актуально, якщо використовувати котлін трохи дальше, ніж формошльопство на андроіді.

Очень интересно, прошу развить мысль.

Давайте розберемо по пунктах
— щодо nullable згідний з вами, автор статті ’накрутив’ себе
— в for не можна робити кастомну умову і інкремент, згідний з автором статті (дивно, що тут в коментарях цього месседжа не зрозуміли, а переключилися на forEach)
— ’=’ - не вираз (expression) — і це дійсно незручно (v1 = v2 = v3 = 0 // Ошибка)
— ’?:’ vs. ’if (value != 0) «Y» else "N"’ - автор не правий, if-else заміняє тернарний оператор
— про автоприведення типів — автор правий, якщо котлін позиціонується, як зручна мова, хай зроблять приведення арифметичних типів
— typealias і женеріки — автор правий, функціонал недопиляний
— по синтаксису для колекцій — очевидно, автор правий

Особисто я теж стикався з такого роду ньюансами і це реально достає, коли не можеш реалізувати красиву задумку, яка мала б підтримуватися фічами мови, але виявляється, що реалізація фічі підхромала. Тому розумію автора статті.

в for не можна робити кастомну умову і інкремент, згідний з автором статті (дивно, що тут в коментарях цього месседжа не зрозуміли, а переключилися на forEach)

Автор привел пример 6ти for-ов тупого итерирования по коллекции — непонятно что конкретно в них ему не понравилось, вот и перешел на forEach как альтернатива его непонятной For
Касательно более сложных случаев — опять же, kotlin идет немного другим путем: все делается цепочками вызовов 100500 вариантов вспомогательных функций типа takeIf, map и т.д. Это решает бОльшую часть задач в kotlin-стиле. Но «я ж привык вот так — значит язык гавно»

Про

typealias і женеріки

 — даже сам автор написал что это проблема вытирания типов в JVM. Поэтому упрекать только котлин в этом случае — глупо.
То, что автор накрутил себя про nullable — вы согласны.
В итоге примерно 70% негатива построены на примерах о nullable и дженериках, которые по факту либо недопонимание автора, либо проблема всего JVM семейства. Остальное — банальная придирка к синтаксису и «я привык так, я так сделать не могу, значит все плохо»

P.S. А еще я думал что люди с 25 годами опыта в отрасли хэйтят новые языки\технологии по более важным темам, таким как performance, наличие-отсутствие глобальной инфраструктуры для разработки и т.д. а не по поводу неудобного синтаксиса

Это все мелочи, которые не оказывают никакого влияния на важные вопросы вроде структуры проекта или интерфейсов классов (кроме generics в очень редких случаях).
И, повторюсь, по моему формошлепскому мнению, говорить что язык отстой из-за того что нет классического for-a, литералов коллекций и возможности написать цепочку присваиваний — очень непрофессионально.

Разобрался? Серьезно?
1) «Убогий for»: автор даже не дошел до кононического подхода котлина forEach, и изобрел какую то несуразную функцию-велосипед For. Его «for (it in list) println(it)» записывается в полне адекватное «list.forEach{ println(it) }»
2) «Истерично-бессмысленная война с null-абле»: я опущу тот момент что автор обращается к глобальной мутабельной переменной, которая кем угодно в любой момент может быть пререзатерта нулом, видно слово «многопоточность» для кого то незнакомо. Но даже если взять этот случай и немного разобраться в языке, то функция
fun F() : Int { if ( value != null ) return 0 return value // Ошибка }
может выглядеть так
fun F() = value ?: 0
что в разы короче и работает.

И так дальше по всем пунктам.
Елинственное с чем могу согласится с автором — это с отсутствием тернарочки. Вот чего реально не хватает, но вроде как есть надежда что ее добавят

1. 25 лет успешно пишет на С, а ума не набрался. Все проблемы из-за того, что он дальше синтаксиса в изучении языка не пошел. (Дочитал только до нуллабл, потом решил все таки осилить полностью и за последний пункт все таки поставлю автору плюс).
2. Закидать в проект кучу джава либ и потом жаловаться, что они не работают на котлине, ну ок.

Ни в коем случае не агитирую переходить на Котлин. Джава прекрасна и проживет еще 100500 лет. Но в ваших ссылках Вы наверно увидели только то, что хотели увидеть. (Кроме того язык очень сильно развивается, чего только корутины стоят.)

Подписаться на комментарии