В 1995 году Sun Microsystems представили Java — объектно-ориентированный язык программирования, основное кредо которого можно сформулировать так: «Написано один раз, работает всегда». В 2011 году как улучшенную альтернативу Java компания JetBrains представила Kotlin — язык с той же философией, но иной реализацией. С тех пор в сообществе программистов между адептами Java и Kotlin ведется непримиримая вражда…

Всем привет! Меня зовут Артем Панасюк, я ведущий backend-разработчик на Java/Kotlin в «Леруа Мерлен». В этом тексте я постараюсь залезть к этим языкам «под капот» и посмотреть, правда ли они такие разные — и в чем преимущества каждого из них.

Присоединяйтесь, будет интересно!

Java vs Kotlin: а есть ли вражда?

Постараемся избежать холиваров и остановимся на фактах. С одной стороны, Java гораздо популярнее: им пользуются 30% всех разработчиков против 9% у Kotlin (или даже 40% против 8% — по другим данным). С другой стороны, Kotlin своим любимым языком называют 63% разработчиков, тогда как Java — всего 44%, а у 56% она вообще вызывает страх. Даже если сделать поправку на то, каким именно образом собиралась статистика и кто попал в базу респондентов, тренд налицо.

При этом сущностно языки похожи. Читабельный код, написанный программистом, компилируется в байткод — и скармливается виртуальной машине, в обоих случаях — Java Virtual Machine. Изначально маркетинг вообще строился на том, что Kotlin — это как Java, только лучше. Сегодня Kotlin “перерос” эти рамки, и сейчас его экосистема позволяет проводить компиляцию не только в байткод Java, но и в нативный машинный код других платформ (iOS, macOS, Windows, Linux, WebAssembly), а также создавать мобильные и десктопные приложения без необходимости использовать JVM в целом.

Поскольку виртуальные машины существуют для самых разных операционных систем, разработчикам удобно использовать эти языки для создания мультиплатформенных приложений. Java в этом смысле стала общим местом при написании софта для Android и Windows/Linux/macOS, Kotlin — для программирования под Android/iOS. Несложно заметить, что Android объединяет оба языка. Долгое время Java использовалась в качестве основного языка, на котором пишутся apk-шки. Но в 2017 году Google передала статус официального языка Android Kotlin’у, аргументировав выбор его большей лаконичностью и производительностью.

Однако, несмотря на поддержку Google’а, лидирующие позиции Kotlin занять не смог. И сегодня оба языка существуют и используются параллельно — во многом благодаря легаси Java и удобству Kotlin. А теперь давайте разберемся, в чем и насколько сильно языки действительно превосходят друг друга.

Что мы будем делать?

В вопросе сравнения на помощь нам приходит тот факт, что и Kotlin, и Java «перевариваются» с помощью Java Virtual Machine (JVM). Код на обоих языках компилируется в *.class и финально в *.jar — а значит, с помощью реверсивного восстановления байткод можно превратить в понятный человеку синтаксис на любом из языков. «Фарш невозможно провернуть назад, и мясо из котлет не восстановишь» — в нашем случае миф, ложь и неправда.

Так и сделаем: воспользуемся опенсорсным декомпилятором Fernflower от JetBrains и путем несложных манипуляций переведем отрывки кода на Kotlin в их аналог на Java. А теперь посмотрим на основы синтаксиса, а также на киллер-фичи Kotlin — лямбда-функции и (кратко) корутины.

Основные конструкции

Здесь стоит сделать небольшую ремарку: названия классов и переменных в Java-коде были даны декомпилятором автоматически, поэтому могут выглядеть немного странно.

Здесь видно, как легко в Kotlin «из коробки» реализован паттерн ДТО — в Java для тех же целей необходимо подключать внешнюю библиотеку или писать лишний код
Здесь видно, как легко в Kotlin «из коробки» реализован паттерн ДТО — в Java для тех же целей необходимо подключать внешнюю библиотеку или писать лишний код
Пример «плохого», или неидеоматичного, кода на Kotlin. Похоже на код разработчика, который пересел на Kotlin после Java
Пример «плохого», или неидеоматичного, кода на Kotlin. Похоже на код разработчика, который пересел на Kotlin после Java
Изящное решение проблемы: когда нужный параметр не передается в метод, то в ход идет дефолтное значение
Изящное решение проблемы: когда нужный параметр не передается в метод, то в ход идет дефолтное значение
Паттерн Singleton в случае Kotlin спрятан в одном служебном слове «Object». На Java придется вспоминать, как он пишется
Паттерн Singleton в случае Kotlin спрятан в одном служебном слове «Object». На Java придется вспоминать, как он пишется

Основной вывод, который сразу бросается в глаза, — лаконичность кода на Kotlin. Собственно, его создатели из JetBrains и адепты из Google использовали этот факт как один из весомых аргументов в пользу преимущества языка. Таким образом, файлы с кодом весят меньше, программистам проще писать, а главное — уменьшается пространство для возможных багов. Понятно, что современные решения помогают программистам находить баги в автоматическом режиме и не дадут скомпилировать забагованный код, но факт остается фактом: чем меньше кода, тем меньше ошибок.

С другой стороны, Kotlin… проигрывает в производительности, вопреки заверениям Google. Да, речь идет о разнице в наносекунды или доли наносекунд — и в большинстве случаев мощности современного железа позволяют разработчикам закрыть на это глаза. Но иногда даже такое отставание может быть существенно.

Наконец, бойлерплейт-код и синтаксический сахар. Первое — это масса шаблонного кода, которая не несет никакой смысловой нагрузки и присутствует в программе исключительно из-за особенностей языка программирования. Одна и та же функциональная единица в Kotlin может занимать две строки, а на Java — десять. И разница в 8 строк — это практически белый шум, ведь по итогу компьютер увидит одно и то же. 

Синтаксический сахар — это, наоборот, использование синтаксических возможностей языка, которые не влияют на содержание, но делают код понятнее и нагляднее. В Kotlin длинные конструкции можно заменять более короткими — это одна из фишек языка. Однако в некоторых случаях такое стремление к минимизации и оптимизации может сыграть злую шутку. И для того, чтобы использовать этот инструмент эффективно, нужно знать не только синтаксис языка, но и его логику, иметь глубинное понимание основ работы.

Лямбда-функции

На момент выхода Kotlin лямбда-функции в Java отсутствовали — их добавили гораздо позднее, из-за чего Kotlin в том числе начал завоевывать популярность. Из-за этого реализация функций в Kotlin независима от Java. И вполне успешна: даже сегодня, после того как лямбда-функции в Java все-таки появились, их реализация в Kotlin имеет меньше ограничений.

Пример функционального программирования и простейшего лямбда-выражения. В Kotlin реализовано без каких-либо Java-библиотек. В Java тот же функционал станет доступен только после восьмой версии
Пример функционального программирования и простейшего лямбда-выражения. В Kotlin реализовано без каких-либо Java-библиотек. В Java тот же функционал станет доступен только после восьмой версии
Функциональный интерфейс из состава стандартной библиотеки Kotlin и его аналог на Java. Сама лямбда в Java-версии видна в методе invoke
Функциональный интерфейс из состава стандартной библиотеки Kotlin и его аналог на Java. Сама лямбда в Java-версии видна в методе invoke

Одно из ключевых отличий лямбда-функций в Kotlin и Java — реализация специализированных классов, которая гарантирует иммутабельность, или постоянство, значений. Подход к типам в Kotlin изначально отличается от Java: любая переменная может быть объектом. Это позволяет, например, захватывать переменные внутрь лямбды — в Java это запрещено.

Поэтому из общих выводов можно выделить два основных. Первый: в целом реализация лямбда-функций на Java и Kotlin примерно идентична. Не с точки зрения синтаксиса, а с точки зрения удобства и производительности. Да, Kotlin в этом смысле чуть более гибок, но и Java к последним версиям стала куда продвинутее. Однако второй вывод: в Java, в отличие от Kotlin, невозможно работать с иммутабельностью, и это качественно другой подход, который позволяет Kotlin снизить число ошибок.

Корутины

Не станем глубоко вдаваться в концепцию корутин — это материал для отдельного текста.

Для решения задач параллельного выполнения есть несколько подходов.

  • Threads — классический подход, который реализован в том числе в Java; ограничен по ресурсам, например, памяти (на один тред выделяется ~2 Mб на уровне ОС).

  • Callback — также реализован в Java; в случае ошибок приводит к «аду обратных вызовов» (callback hell), в котором катастрофически сложно разобраться.

  • Future/Promise/RX — особый раздел, реактивное программирование, требует больших человеческих ресурсов на погружение.

  • Coroutines — просты по кодовой базе и синтаксису, легковесны и не имеют таких жестких ограничений, как Threads.

Сама концепция корутин не нова — название появилось еще в 1958 году. Поддержку решения предоставляют многие современные языки программирования: C#, Go, Python, Ruby, Kotlin… Но не Java.

И именно корутины во многом обусловили выбор Kotlin в качестве официального языка разработки на Android — вместо Java — и популярность языка в целом. Не так давно схожий функционал был реализован в Java посредством Project Loom, однако момент уже упущен.

Итоги

Мы убедились, что сравнение этих языков программирования — тема скорее кликбейтная и холиварная, чем по-настоящему содержательная: сегодня существенных различий между Java и Kotlin нет. В последних обновлениях функционал языков во многом стал идентичен, а особенности синтаксиса и разницу в производительности сложно назвать явным преимуществом любого из них. Да, Java воспринимается чуть более устаревшей, а Kotlin — модным-молодежным, но по существу у обоих есть свои плюсы и минусы, фанаты и хейтеры.

В «Леруа Мерлен» значительная часть ПО пишется на Kotlin. Во многом это продиктовано историческим контекстом: активный период цифровизации и развития собственных ИТ-сервисов пришелся на «золотой» период развития языка, когда тот отвоевывал у Java место на рынке, но и сегодня его использование позволяет нам быть актуальными в своем стеке.

На Kotlin, например, разрабатывалось мобильное приложение под iOS и Android. И я по собственному опыту (Kotlin стал моим вторым языком после Java) и по опыту коллег вижу, что неполное понимание работы языка мешает использовать его на сто процентов.

Те, кто учит Kotlin как второй язык, часто начинают писать на нем, пользуясь той же логикой, что пользовались с Java. Это не дает языку полностью раскрыть свой потенциал — код работает (и это плюс Kotlin — его гибкость), но не так эффективно. И чтобы добиться от своей работы максимума продуктивности, нужно понять, что находится у Kotlin «под капотом». Да, при желании можно забить шуруп молотком — он сработает так, как в этот момент нужно человеку. И тем не менее, если использовать его по назначению, КПД у процесса вырастет многократно.

Так что помимо сравнения языков моей задачей было разобраться в том, как на самом деле работает Kotlin, как его логику воспринимает компьютер. Здесь его родство с Java сыграло нам на руку — и я надеюсь, что через сравнение мы смогли лучше понять, почему на самом деле разработчики «любят» Kotlin. А что по этому поводу думаете вы? Делитесь своим мнением в комментариях.

Комментарии (18)


  1. pokrovskiy_199
    01.04.2024 11:29
    +2

    Я заметил ,что одна из основных особенностей, которая сразу бросается в глаза, - это безопасность от нулевых ссылок. В Kotlin вы пишете менее подверженный ошибкам код благодаря строгой системе типов, которая обеспечивает безопасность от нулевых ссылок. Но это также означает, что в байткоде появляются дополнительные проверки null для обеспечения безопасности, что в некоторых случаях может сказаться на производительности.


    1. koperagen
      01.04.2024 11:29

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


  1. Fancryer
    01.04.2024 11:29
    +3

    Про дата-классы не согласен, в Java вполне себе есть рекорды. Про потоки тоже было странно, поскольку виртуальные появились как JEP в JDK 19, а сейчас они уже стали частью стандарта. Почему бы не сравнивать Kotlin 1.9.22-1.9.23 с Java 22?


    1. arty79 Автор
      01.04.2024 11:29

      Основная мысль текста как раз в том, что и там, и там появились одни и те же фичи. Просто в разное время. По итогу Kotlin сегодня уже в принципе отвязан от JVM, и сравнивать их напрямую особого смысла нет. Неправильно же сравнивать Python и Java только потому, что оба языка объектно-ориентированные.
      Речь про то, что, появившись в нужный момент как улучшенная версия Java, Kotlin смог занять свое место под солнцем - и тогда его преимущество в функциях оказалось решающим.


      1. ris58h
        01.04.2024 11:29
        +3

        Но с первого же слайда вы про фичу в Java умалчиваете и утверждаете что "в Java для тех же целей необходимо подключать внешнюю библиотеку или писать лишний код", которое ложно уже несколько лет как.


        1. arty79 Автор
          01.04.2024 11:29

          Kotlin всегда, даже в относительно старых версиях, имел эту фичу, а Java, хотя и имеет фичу в версиях начиная с 14, но в бОльшей части энтерпрайз проектов процент новых версий не близок к 100 - отчет за 2023 год дает примерно 89% проектов все еще ниже 14 версии


    1. BugM
      01.04.2024 11:29
      +1

      Тогда сравнение в пользу Джавы может получиться. Рекорды, паттернматчинг и виртуальные потоки закрывают большую часть того что продавало Котлин. Разнообразные нотнулл аннотации закрывают остаток. Вот бы еще все основные бибилиотеки ими разметили…


      1. Sigest
        01.04.2024 11:29
        +3

        Да не то только null protection фишка котлина. Функциональное программирование в котлин в разы удобнее. Эти collection.stream()…..collect(…) в java просто ужас какой многословный по сравнению с collection.map{…} А если взять функции-расширения, перегрузка операторов, всякие делегаты с синглтонами, то и получается, что джаве еще далеко до удобства котлина. Сам пишу по работе на обоих языках примерно 50/50, не хейтю джаву, но такое вот субъективное мнение


        1. DenSigma
          01.04.2024 11:29

          zip кстати в Java нет.


          1. Sigest
            01.04.2024 11:29

            Как нет? У стримов есть zip метод

            Streams.zip(Collection1, Collection2, лямбда).как_всегда_collect(...)


  1. ruslun
    01.04.2024 11:29

    У меня вопрос к опытным разработчикам: цель стать разработчиком на Kotlin, надо сначала хорошо изучить Java или можно сразу браться за KT ?


    1. arty79 Автор
      01.04.2024 11:29

      Думаю, если есть практика владения другим объектно-ориентированным языком, то можно начать сразу с Kotlin; если же такой практики нет, то, возможно, лучше начать погружаться с Java


      1. LordDarklight
        01.04.2024 11:29

        Начать с Kotlin как раз проще, чем с Java не имея практики ООП.

        Вот, с С++ безусловно проще на Java перейти, но ещё лучше - перейти на C# - а потом уже, по необходимости, на Kotlin


    1. m0rtis
      01.04.2024 11:29

      Если цель - стать разработчиком на Котлин, то изучать Java совершенно необязательно. Если будете писать под jvm-таргет, лучше просто изучить отдельно именно особенности jvm.


    1. Sigest
      01.04.2024 11:29

      Я бы выучил джаву. Взаимодействовать, скорее всего, придется. Ну там полазить в кишках библиотек и т.д. А идеоматически Котлин все дальше и дальше от Джавы в плане стиля кода. Я раньше иногда понять не мог на каком языке написан код - на джаве или котлине. Сейчас, если писать в стиле Котлин, как-то уже и совсем не Джава стиль получается.


  1. DenSigma
    01.04.2024 11:29

    Сравните пожалуйста с последней версией Java. С рекордами в частности. Какой бойлерплейт?

    Второе - не понятен смысл маски в конструкции со значением по умолчанию. Код может быть простейший

        private static class cl {
            private final int DEFAULT_X = 42;
            private void foo(int x) {
                System.out.println(x);
            }
            
            private void foo() {
                foo(DEFAULT_X);
            }        
        }

    Вообще, по опыту работы с Delphi, ничего хорошего в указании значений параметров по умолчанию в объявлении методов нет. Это протаскивание деталей реализации в объявление. Это раз. Второе (я не в курсе) - как эти параметры по умолчанию стыкуются с интерфейсами? Объявление методов должно быть ТОЧНО соответствовать объявлениям интерфейсов - в этом смысл интерфейсов. А в разных классах в объявлениях одних и тех-же методов могут быть разные константы. Это нарушение идеологии интерфейсов. В третьих, это просто путаница, когда в одних случаях нужно использовать параметры по умолчанию и свои параметры, а в других случаях - другие параметры по умолчанию и свои параметры. Это невозможно разрулить, когда параметров штук пять-десять. Java с помощью библиотек предлагает изящное решение - билдеры (правда надо определять класс параметров).


    1. HemulGM
      01.04.2024 11:29

      С интерфейсами все там стыкуется, если речь о Делфи. Для интерфейса такой метод - это метод с набором всех параметров и не важно, что какие-то заданы по умолчанию.

      Параметры по умолчанию достаточно редко используются, но это все равно удобно.

      Кстати, в Делфи в конструкторе я часто использую вариант такой:

      constructor TMyObject.Create(Proc<TMyObject>);
      begin
        inherited Create;
        Proc(Self);
      end;
      
      // и при вызове
      var Obj := TMyObject.Create(
        procedure(MyObj: TMyObject)
        begin
          MyObj.Prop1 := 1;
        end)


    1. Sigest
      01.04.2024 11:29

      Во-первых, В Java и Kotlin интерфейсы необязательны. В Kotlin даже классы необязательны (хотя в байткоде обернутся все же в классы функции и переменный объявленые вне классов). Поэтому метод с параметрами необязательно должен стыковаться с каким-то там интерфейсом. Даже если у класса, в котором объявлен метода с параметрами по умолчанию, есть интерфейс - ну и ладно. Я лично так делал и не страдал от нарушения идеологий. Да и не понимаю я, в чем нарушение? Интерфейс - это контракт на структуру и результат, а не на данные или способ подкапотного функционирования этой структуры. Данные как раз предполагаются быть разными в имплементациях, раз уж интерфейс объявлен.

      Во-вторых, это плохо иметь 10 параметров в методе. И в Котлине для таких случаев тоже надо использовать билдеры, хоть самому писать, хоть с использованием тех же библиотек из Джавы