Kotlin глазами Java-разработчика

Привет, хабр! Сегодня я хочу рассказать про свой опты взаимодействия с языком kotlin.

Представлюсь — я java разработчик, работаю крупном банке, создаю (и поддерживаю существующие) микросервисы.

Небольшая ремарка: я не собираюсь становиться Android разработчиком, ни сейчас, ни в будущем, поэтому, когда я заинтересовался новым языком, не принимал в расчет аргументы про различные удобства мобильной разработки на нем, и руководствовался только удобством языка в целом для бэкэнда.

Итак, почему я решил изучить kotlin. Ну, во-первых, прожужали все уши, мол сокращение объема код, лаконичность, читаемость и сахар.

Во-вторых, Kotlin полностью совместим с Java. Для понимания, ниже скриншот того, как выглядит содержание одного из пакетов intellij плагина git4idea:

кусок структуры исходников плагина git4idea

кусок структуры исходников плагина git4idea

(Java и Kotlin классы идут вперемешку, код при этом остается читаемым (местами))

Да, проекты можно мигрировать с Java на Kotlin постепенно, как по мне — киллер-фича.

Так что, я подумал, почему бы и нет.

Вот вещи, к которым я привык за свои полгода пет-проектирования на kotlin, и которыми лично мне удобно пользоваться:

  1. Закрытые для расширения классы и методы. Все по умолчанию final, в отличии от Java, где, наоборот, любой класс открыт для расширения. Kotlin же придерживается принципа, описанного еще в Effective Java Дж.Блоха, который звучит так:

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

    Для того, чтобы сделать класс в kotlin расширяемым, мы должны использовать ключевое слово open, которое не обладает транзитивным свойством — это означает, что когда Class2 наследуется от open class Class1, мы не сможем унаследовать Class3 от Class2, без навешивания на него ключевого слова open.

  2. Null-safe. Возможность на уровне компиляции разрешить/запретить (по умолчанию) присвоение null в переменную — лайк.

    var a: String = "abc"
    a = null                // compile error
    
    var b: String? = "xyz"
    b = null                // no problem
  3. Геттеры и сеттеры по умолчанию. Любое поле не извлекается напрямую, более того под капотом оно является 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 

    Примеры скоммуниздил

  4. Однострочные методы. Буду краток: при объявлении метода вида:

     fun doSomething(): String{
        return "doing something”
    } 

    Kotlin позволяет убрать явное объявление возвращаемого типа, фигурные скобки и ключевое слово return

    Выглядеть это начинает следующим образом:

     fun doSomething() = "doing something”

    Сначала было непривычно (особенно когда такие объявления идут после блока инициализации полей класса), но потом привык

  5. Упрощена большая вложенность при сравнении строк и любых других сложных типов в 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
    }
  6. Именованные аргументы. В сочетании с аргументами по умолчанию именованные аргументы избавляют от необходимости использовать Строителей (паттерн 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)
    } 
  7. Перегрузка операторов (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)

     пример позаимствован отсюда

  8. Функции-расширения (Extension Functions)

    Если коротко: мы можем дополнить существующие классы нашим функционалом без наследования, и относиться к этому функционалу, как к родному на протяжении всей дальнейшей программы.

    fun String.format() = this.replace(' ', '_')
    
    val str = "hello world"
    val formatted = str.format()

    или, например:

     val result = str.removeSuffix(".txt")

    (В java я бы вынес это в статический метод какого-нибудь класса StringUtils)

    Удобно, не так ли? Тем более что IDE подсказывает нашу функцию среди прочих всплывающих.

  9. Работа с 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 — паттерны проектирования, без воды и шелухи

© Habrahabr.ru