Обложка: Сравнение Kotlin и Java при написания backend-приложений

Сравнение Kotlin и Java при написания backend-приложений

Я разработчик больших и маленьких бэкенд систем в крупных компаниях. Приходилось писать как отдельные сервисы, которые общаются с другими бэкенд сервисами разного уровня глубины, так и сервисы, работающие в связке с фронтом. Раньше для написания кода я использовал  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 или нет.

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

Перейти к регистрации