В статье на tproger.ru бэкенд-разработчик поделился своим опытом использования Kotlin и Java. Представляем этот рассказ вашему вниманию.
Я разработчик больших и маленьких бэкенд-систем в крупных компаниях. Приходилось писать как отдельные сервисы, которые общаются с другими бэкенд-сервисами разного уровня глубины, так и сервисы, работающие в связке с фронтом. Раньше для написания кода я использовал Java + Spring.
А после поменял проект и столкнулся с Kotlin именно в бэкенде. И хочу поделиться преимуществами Kotlin и отличиями Kotlin от Java в абсолютно одинаковых задачах.
Забегая вперёд скажу, что колоссальной разницы, из-за которой нужно срочно переписывать всё на Kotlin, нет. Но есть огромное количество фич, которые делают разработку быстрее, проще и безопаснее.
На текущем проекте весь новый функционал мы с командой пишем на Kotlin, параллельно переписывая старые куски Java-кода n-летней давности. На Kotlin эти куски получаются гораздо более читабельными и короткими.
Простота интеграции в уже существующий проект, написанный на Java
Если вы только присматриваетесь к Kotlin для бэкенда, то учитывайте, что в окружении, которое запускает ваш проект на Java 8, можно без танцев с бубном запустить скомпилированный проект на Kotlin. Да, на той же jvm, на том же окружении и с минимумом усилий.
Для меня было открытием, что даже в рамках одного приложения могут быть классы на Java и Kotlin. Вся магия происходит на этапе компиляции. В зависимости от настроек, можно указать что собирать первым: Kotlin-классы или Java-классы.
Сейчас доступна компиляция Kotlin-исходников в байткод от Java LTS — 8, 11 и (пока экспериментально) 16.
Инициализация и логика работы с DTO классами
Пример избитый, но максимально наглядный.
data class Cat(val name: String, val color: String, val height: Int)
И теперь то же самое на Java:
public class Cat { private final String name; private final String color; private final Integer height; public Cat(String name, String color, Integer height) { this.name = name; this.color = color; this.height = height; } public String getName() { return name; } public String getColor() { return color; } public Integer getHeight() { return height; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Cat cat = (Cat) o; return Objects.equals(name, cat.name) && Objects.equals(color, cat.color) && Objects.equals(height, cat.height); } @Override public int hashCode() { return Objects.hash(name, color, height); } }
Мне кажется, здесь даже комментарии излишни.
Указатели data class в Kotlin по-умолчанию подразумевают наличие getter, setter (для var полей), equals, hashcode, toString для всех полей. При желании можно переопределить каждый из этих методов по-своему, но требуется это крайне редко.
class Cat(val name: String, val color: String, val height: Int) { override fun toString(): String = "overridden toString" }
Null safety
В Kotlin требуется явно указывать, может ли тот или иной метод вернуть null. Таким образом, можно считать, что все данные уже обернуты в аналог Optional. И NullPointerException будет вам встречаться настолько редко, что вы успеете по нему соскучиться.
fun saveReviewNullable(r: Review): Review? = reviewRepository.save(r) fun bar(r: Review) { val savedReviewNullable: Review = saveReviewNullable(r)!! // Есть риск NPE - не феншуй val savedReviewNotNull: Review = saveReviewNullable(r) ?: Review() // феншуй }
Выделение основного конструктора
Суть в чём: есть основной (Primary) конструктор и вспомогательные (Secondary). Вспомогательные обязаны вызывать основной как конструктор родительского класса.
class Cat(val name: String, val color: String, val height: Int) { constructor(name: String) : this( name = name, color = "fixed color", height = 10 ) }
Явное объявление изменяемых и неизменяемых полей
Следующее преимущество: в Kotlin получаются довольно простые и элегантные конструкции. Если требуется, чтобы поле dto можно было менять, то для его объявления используется var. Тогда будет создан метод setter и поле не будет final.
А если нужно сделать поле иммутабельным, для объявления следует использовать val. Смотрится очень красиво и просто. Плюс не нужно следить за вспомогательными методами.
Пример: поля цвета и роста можно изменить после создания, а имя — только при инициализации объекта:
data class Cat(val name: String, var color: String, var height: Int)
Immutable коллекции по умолчанию
То, что появилось в Java несколько позже, уже давно было в Kotlin — создание коллекций сразу иммутабельными.
val list = listOf("one", "two") val map = mapOf(1 to "one", 2 to "two") val set = setOf(1, 2 ,3)
Любые изменения с этими коллекциями будут создавать новую иммутабельную коллекцию после преобразования:
val list = listOf("one", "two") val list2 = list.plus("three")
Но изменить какой-то элемент отдельно не получится. Для классических мутабельных коллекций используется явно изменяемые аналоги:
val list = mutableListOf("one", "two") val map = mutableMapOf(1 to "one", 2 to "two") val set = mutableSetOf(1, 2 ,3)
Работа со сложными классами методами примитивов
Преимущество Kotlin, которому не перестаю радоваться — возможность использовать операторы для базовых операций к сложным классам. Если нужно сложить BigDecimal числа — берёшь и пишешь их через плюс. Не нужно явно вызывать метод у первого слагаемого.
То же самое с массивами: хочешь удалить элемент из мутабельного массива – пишешь массив минус этот элемент. И если элемент присутствует, то он удаляется.
val a = BigDecimal(1) val b = BigDecimal(2) val sum = a + b
В Java нужно вызывать специальный метод:
BigDecimal a = new BigDecimal(1); BigDecimal b = new BigDecimal(2); BigDecimal sum = a.add(b);
Аналогичные приёмы работают и с более сложными классами, например с коллекциями:
val list = listOf("one", "two") - "one" // list - коллекция из элемента "two"
Возможно писать однострочные методы действительно в одну строку
Если метод простой и состоит из одной операции или из цепочки операций, записанных в одну строчку, то не обязательно писать фигурные скобки и return. Прямо пишешь:
fun getReviewByTitle(title: String): List<Review> = reviewRepository.getAllByTitle(title)
Вместо Java-варианта:
public List<Review>(String title) { return reviewRepository.getAllByTitle(title); }
Контекстные функции
Интересные движения в сторону функционального программирования в духе выделения контекста: лямбды можно вертеть, как хочешь.
Есть функции let, apply, also, with, run. Из-за их обилия вначале возникает вопрос: что подходит для конкретного кейса. Но когда привыкаешь, становится непонятно как раньше жил без них.
Простой пример: взять результат и как-то его обработать:
fun saveReview(review: Review): Review = reviewRepository.save(review) fun bar(r: Review) = saveReview(r).let { it.comment + it.title }
Либо инициализировать объект и дополнительно проинициализировать его var поля:
class Cat(val name: String, val height: Int) { var color: String? = null } fun bar() = Cat("Fred",10).apply { color = daoService.getPopularColor() }
Кто-то может сказать, что это сахар сахарный, и будет прав. Но по себе скажу: если большое количество шаблонных вещей входит в язык, и о них не нужно постоянно думать — процесс разработки становится проще, количество ошибок — меньше. Но нужно чаще думать про код-стайл, потому что без него навертеть можно многое 🙂
Сейчас я по-прежнему продолжаю писать и видеть код на Java, но в 99% случаев это связано с образовательными программами, в которых принимаю участие: профессия Java-разработчика на Hexlet и различные вебинары для онлайн школ и курсов. Так что я постоянно сравниваю два языка и радуюсь, что рабочие проекты пишу на Kotlin.
Советую попробовать каждому — как минимум на pet-проекте — чтобы понять, подходят вам парадигмы Kotlin или нет.
[customscript]techrocks_custom_after_post_html[/customscript]
[customscript]techrocks_custom_script[/customscript]