Kotlin глазами Java-разработчика
Привет, хабр! Сегодня я хочу рассказать про свой опты взаимодействия с языком kotlin.
Представлюсь — я java разработчик, работаю крупном банке, создаю (и поддерживаю существующие) микросервисы.
Небольшая ремарка: я не собираюсь становиться Android разработчиком, ни сейчас, ни в будущем, поэтому, когда я заинтересовался новым языком, не принимал в расчет аргументы про различные удобства мобильной разработки на нем, и руководствовался только удобством языка в целом для бэкэнда.
Итак, почему я решил изучить kotlin. Ну, во-первых, прожужали все уши, мол сокращение объема код, лаконичность, читаемость и сахар.
Во-вторых, Kotlin полностью совместим с Java. Для понимания, ниже скриншот того, как выглядит содержание одного из пакетов intellij плагина git4idea:
кусок структуры исходников плагина git4idea
(Java и Kotlin классы идут вперемешку, код при этом остается читаемым (местами))
Да, проекты можно мигрировать с Java на Kotlin постепенно, как по мне — киллер-фича.
Так что, я подумал, почему бы и нет.
Вот вещи, к которым я привык за свои полгода пет-проектирования на kotlin, и которыми лично мне удобно пользоваться:
Закрытые для расширения классы и методы. Все по умолчанию final, в отличии от Java, где, наоборот, любой класс открыт для расширения. Kotlin же придерживается принципа, описанного еще в Effective Java Дж.Блоха, который звучит так:
«классы и методы должны быть закрыты и не подлежат расширению или переопределению, если только у нас нет веской причины для их расширения или переопределения. И когда мы решаем, что наш класс должен быть открыт для расширения, мы должны документировать последствия переопределения любого метода.» Источник
Для того, чтобы сделать класс в kotlin расширяемым, мы должны использовать ключевое слово open, которое не обладает транзитивным свойством — это означает, что когда Class2 наследуется от open class Class1, мы не сможем унаследовать Class3 от Class2, без навешивания на него ключевого слова open.
Null-safe. Возможность на уровне компиляции разрешить/запретить (по умолчанию) присвоение null в переменную — лайк.
var a: String = "abc" a = null // compile error var b: String? = "xyz" b = null // no problem
Геттеры и сеттеры по умолчанию. Любое поле не извлекается напрямую, более того под капотом оно является private (по умолчанию) и может быть доступно только геттеру. Когда вы пытаетесь получить поле вашего объекта, вы на самом деле вызываете метод get
val isEmpty: Boolean
эквивалентно следующему Java коду:
private final Boolean isEmpty; public Boolean isEmpty() { return isEmpty; }
Ну, и:
var someProperty: String = "defaultValue"
также эквивалентно:
var someProperty: String = "defaultValue" get() = field set(value) { field = value }
При желании, разумеется, можно ограничить доступ к геттерам или сеттерам:
var isEmpty: Boolean = true private set
Примеры скоммуниздил
Однострочные методы. Буду краток: при объявлении метода вида:
fun doSomething(): String{ return "doing something” }
Kotlin позволяет убрать явное объявление возвращаемого типа, фигурные скобки и ключевое слово return
Выглядеть это начинает следующим образом:
fun doSomething() = "doing something”
Сначала было непривычно (особенно когда такие объявления идут после блока инициализации полей класса), но потом привык
Упрощена большая вложенность при сравнении строк и любых других сложных типов в If — чуваки зашили equals в == (начинает работать, если переопределить метод equals явно, либо если пометить класс ключевым словом data.)
Добавлю, что если уж вам нужно сравнить именно ссылки на объекты, в kotlin есть отдельный оператор для этого: тройное равно ===
Java:
if (str1.equals(str2.equals(errString)? "default":str2)){ //... }
Kotlin:
if (str1 == if (str2 == errString) "default" else str2) { //... }
Сравнение классов:
Java:
@EqualsAndHashCode @AllArgsConstructor public class MyClass { private String name; private int value; } public static void main(String[] args) { MyClass first = new MyClass("name", 5); MyClass second = new MyClass("name", 5); System.out.println(first.equals(second));//true System.out.println(first == second);//false }
Kotlin:
data class MyClassK(var name: String?, var value: Int) fun main() { val first = MyClassK("name",10) val second = MyClassK("name",10) println(first == second)//true println(first === second)//false }
Именованные аргументы. В сочетании с аргументами по умолчанию именованные аргументы избавляют от необходимости использовать Строителей (паттерн Builder)
Вместо следующего Java-кода:
@Builder //lombok annotation public class MyClass { private String name; private int value; private double rating; } MyClass myClass = MyClass.builder() .name("MyJavaObj") .value(10) .rating(4.99f) .build();
На kotlin мы можем сделать так:
class MyClassK(name: String? = null, value: Int = 0, rating:Double = 0.0) fun main() { var myClassK = MyClassK(value = 10, name = "MyKotlinObj", rating = 4.99) }
Перегрузка операторов (Operator Overloading)
В kotlin заранее определен набор операторов, которые можно перегружать для улучшения читабельности:
data class Vec(val x: Float, val y: Float) { operator fun plus(v: Vec) = Vec(x + v.x, y + v.y) } val v = Vec(2f, 3f) + Vec(4f, 1f)
пример позаимствован отсюда
Функции-расширения (Extension Functions)
Если коротко: мы можем дополнить существующие классы нашим функционалом без наследования, и относиться к этому функционалу, как к родному на протяжении всей дальнейшей программы.
fun String.format() = this.replace(' ', '_') val str = "hello world" val formatted = str.format()
или, например:
val result = str.removeSuffix(".txt")
(В java я бы вынес это в статический метод какого-нибудь класса StringUtils)
Удобно, не так ли? Тем более что IDE подсказывает нашу функцию среди прочих всплывающих.
Работа с NPE в цепочке вызовов. Если мы хотим извлечь из сложного, составного объекта, какую-то маленькую часть, но при этом нет гарантий, что на этом пути нас не ждет null, нам приходится делать следующее:
Java:
try{ Status status = journal.getLog().getRow(0).getStatus(); if(status == null) throw new NullPointerException("null status detected in log”); } catch(NullPointerException e){ status == Status.ERROR; //logger.error("Journal is not correct”); }
Kotlin:
var status: Status? = journal?.log?.row?.status status = status ?: Status.ERROR
либо
var status: Status? = journal?.log?.row?.status if(status.isNull()){ status = Status.ERROR logger.error("Journal is not correct”) }
Не затронул здесь корутины, поскольку я недостаточно с ними знаком, да и в целом у меня нет большого опыта работы с многопоточкой, чтобы я мог высказывать свое экспертное мнение. Но вообще, посмотрел доклад и вам советую. Может соберусь, изучу тему и напишу что-то про это.
Как же это прекрасно, что теперь я могу не бояться NPE в цепочке вызовов. В Java, как можно видеть, приходится оборачивать небезопасную цепочку вызовов в громоздкий try-cath, но черт побери, иногда я хочу, чтобы при возврате null где-то в цепочке, в переменную просто присваивался null. Именно этот функционал мне и дает kotlin.
Лирическое завершение:
Вообще нахожусь в больших раздумьях насчет языка: я очень долго программирую на Java и она мне, как то роднее, что ли.
(Когда вижу в проекте файлы с функциями, отдельно от классов, периодически дергаюсь.)
Много нюансов и изменений, как ни крути, хотя и совместимость.
Слушал недавно несколько подкастов с гостями — разработчиками и продактами JetBrains, ребята так увлеченно рассказывали про язык, его перспективы и горизонты, мол как Java, только лучше, Kotlin Multiplatform и тд.
Но все-таки остается фактом, что массово kotlin в бэкэнд не пошел, а занял нишу мобилок, хотя может это я чего-то не знаю. Может за моей спиной в проде все тайно уже переписали свои жаба проекты на kotlin, а мне не сказали.
Да и вообще есть ли такое понятие, как основной язык? Может не надо так к этому относиться, а придерживаться принципа «под каждую задачу свой инструмент». Но тогда под какие задачи бэкэнда kotlin, а под какие java? Или все-таки «прокачанная java»?
Очень хотелось бы услышать чужой опыт взаимодействия с этими языками в комментариях, желательно именно ребят, которые занимаются бэкэндом в кровавом энтерпрайзе, а не мобильщиков, хотя рад всем.
Был ли опыт миграции больших проектов с одного языка на другой? Если да, то какой? Если нет, то почему?
Может вы поменяли лично свой основной язык? Почему? Сахар и отсутствие точек с запятой? Или более глобальные причины?
(Пришла в голову мысль, что мне синтаксический сахар не кажется киллер-фичей какого-либо языка, ведь когда ты привык писать так, а другие привыкли так читать, это перестает быть проблемой)
Источники:
https://habr.com/ru/companies/otus/articles/532270/ — Проверка на равенство в Kotlin
https://youtu.be/rB5Q3y73FTo? si=piObKnscuv1S9vtg — Роман Елизаров. Корутины в kotlin
https://habr.com/ru/companies/vk/articles/329294/ — обзор фич kotlin от ВК
https://stackoverflow.com/questions/37906607/getters-and-setters-in-kotlin — геттеры и сеттеры. Тред на Stack Overflow
https://www.baeldung.com/kotlin/open-keyword — ключевое слово open
https://github.com/amitshekhariitbhu/from-java-to-kotlin — прикольный репо-обучалка в формате Java vs Kotlin
https://radioprog.ru/category/183 — паттерны проектирования, без воды и шелухи