Всем привет, меня зовут Клименко Руслан. Сейчас я ведущий разработчик програмного обеспечения в одной из украинских аутсорсинговых компаний, 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, чтобы получить такой же результат.
Пошагово процесс декомпиляции выглядит следующим образом:
- Пишем программку на Kotlin.
- Компилируем её с помощью компилятора kotlinc.
- Декомпилируем приложение в 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 этой осенью. Поэтому если вы молоды, горячи и очень хотите, то можно вступить в нашу группуи смиренно ждать анонса курса.
На этом сегодня всё. Спасибо за внимание. Будьте счастливы.