
Недавно я написал рассказ, рассказывающий обо всех изменениях, которые были внесены в язык Java после Java 11. Хотя он доказывает, что язык Java все еще развивается, он находится под давлением других языков JVM, в первую очередь Kotlin. В этой статье я представлю самые очевидные улучшения, которые Kotlin привносит в игру.
Что такое Котлин
Если вы читаете эту статью, я предполагаю, что вы знаете, что такое Kotlin, или, по крайней мере, слышали о нем. Просто чтобы убедиться, что мы все на одной странице, позвольте мне дать вам краткое введение.
Котлин…
- …это статически типизированный язык, работающий на JVM
- …использует функции таких языков, как Scala, C#, Groovy, Pascal (и, конечно же, Java)
- … разработан JetBrains, компанией, разработавшей такие IDE, как IntelliJ, GoLand, RubyMine и AppCode, и это лишь некоторые из них.
Язык был разработан с учетом ряда целей. Это должно было быть:
- Краткость (Java борется с многословием)
- Практический (используйте современные языковые конструкции)
- Безопасно (можно ли избавиться от ужасного NPE…?)
- 100% совместимость с Java (поскольку, скажем прямо, Java по-прежнему является бизнес-языком №1. Используйте эту установленную базу и попытайтесь расширить ее)
Хотя Kotlin изначально разрабатывался для работы на JVM, с тех пор он распространился на ряд других платформ. Во-первых, это доминирующий язык для разработки под Android, тем более что Google принял его в качестве основного языка для Android. Но Котлин на этом не остановился! Благодаря инициативе Kotlin Native вы можете создавать собственные исполняемые файлы (да, это верно, JVM не требуется!) для Linux, MacOs, Windows (вроде…), iOS (и tvOS и watchOS). Мы подробнее коснемся этого в будущем наборе специальных статей, где мы создадим приложение для Linux, MacOs и Windows.
После этого краткого введения давайте представим улучшения, которые Kotlin добавляет поверх языка Java. Пожалуйста, имейте в виду, что здесь я говорю о языковых функциях, то есть о вещах, которые делают жизнь разработчиков проще, приятнее и в целом веселее.
Эти особенности:
- Вывод типа
- Нулевая безопасность
- Характеристики
- Именованные аргументы и аргументы по умолчанию
- Классы данных
- Методы расширения
- Корутины
Причина 1: вывод типа
Kotlin поддерживает полный вывод типов как для локальных переменных, так и для переменных экземпляра. Он также поддерживает вывод типов для лямбды. Хотя Java поддерживает вывод типа локальной переменной, начиная с Java 10, и синтаксис локальной переменной для лямбда-параметров, начиная с Java 11, для переменных экземпляра такой поддержки не существует. И, возможно, именно там они больше всего нужны. Kotlin поддерживает весь диапазон, как показано в этом фрагменте кода.
Причина 2: обнуляемость
Тони Хоар был человеком, который изобрел нулевую ссылку еще в 1965 году. Многие языки приняли идею нулевой ссылки, в том числе и Java. Однако объекты, способные принимать значение null, вызывали много проблем. Потому что в основном вы говорите: это ни на что не указывает. Или, может быть, это так! Это почти эквивалент кота Шрёдингера объектно-ориентированных вычислений.
Если вы вызовете метод для объекта с нулевой ссылкой (например, person.getName(), где person содержит нулевую ссылку), это приведет к сбою вашего приложения. Даже сегодня это вызывает столько вопросов, что Хоар назвал изобретение нулевой ссылки своей ошибкой на миллиард долларов (хотя на полном серьезе: миллиард долларов — это грубая недооценка реальных затрат).
В Kotlin все переменные по умолчанию не обнуляемые, то есть вы должны инициализировать их значением. Значение, отличное от нуля, то есть 😉
Таким образом, все переменные в приведенном выше фрагменте не являются нулевыми переменными. Попытка установить для них значение null вызовет жалобу компилятора Kotlin. Это уже намного лучше, не так ли? Это не взрывается во время выполнения, но вам уже говорят во время компиляции.
Но иногда вы не можете обойтись без переменных, допускающих значение null: возможно, вам придется смешать код Kotlin с существующим кодом Java. Или вы можете использовать стороннюю библиотеку, написанную на Java. Итак, Kotlin позволяет вам определять переменные, допускающие значение NULL, но вы должны делать это явно, тем самым заставляя себя признаться в своем преступлении.
Постфикс типа с помощью ? помечает его как тип, допускающий значение NULL. Обратите внимание, что String и String? это два совершенно разных типа для компилятора, которые не имеют никакого сходства.

Если бы мы хотели вызвать метод для этих строк, компилятор точно знал бы, что это безопасно делать для nonNullableName, поскольку оно не может быть нулевым. nullableName, с другой стороны, может содержать нулевое значение, и вызов метода для него приведет к хаосу. Таким образом, если вы этого хотите, компилятор хочет убедиться, что вы знаете, что делаете. Вы должны использовать ненулевой утвержденный вызов, также называемый двойным вызовом. Это делается путем добавления к имени переменной двойного восклицательного знака (!!) как такового:
Обратите внимание, что строка 7 приведет к возникновению исключения нулевого указателя (NPE), поскольку вы добровольно вызываете метод для объекта, содержащего нулевое значение.
Чтобы сделать жизнь немного проще, а код более читабельным и чистым, мы можем использовать оператор safe-call (?.)
В строке 6, поскольку мы используем '?.', метод в верхнем регистре никогда не будет вызываться для nullableName (безопасный вызов в основном означает: вызывать метод для объекта только в том случае, если сам объект имеет ненулевое значение ). Хорошая работа! Мы предотвратили возникновение NPE.
В Kotlin есть много других методов и лямбда-выражений, облегчающих работу со значениями, допускающими значение NULL, но они выходят за рамки этой статьи.
Лучше всего помнить, что если вы можете использовать только ненулевые типы, вашему коду не нужны эти приемы, и во время выполнения не возникнет NPE.
Причина 3: Свойства
Мы уже встречались со свойствами в нашем первом примере. Там мы определили «name», «age» и «isFemale». Мы назвали их (экземпляр) переменными, что на самом деле не совсем так: это свойства. Какая разница? Ну, во-первых, их можно определить как изменяемые или неизменяемые. И нам не нужно (но мы можем) определять для них геттеры и сеттеры.
Определение геттеров и сеттеров всегда было очень громоздким в Java и приводило к большому количеству кода. Вы могли позволить IDE сгенерировать их для вас, но это все равно означало, что было много строк кода. И при осмотре такого класса приходится продираться через все это, чтобы быть уверенным, что понимаешь весь смысл и ничего непредвиденного не происходит. Поэтому люди начали использовать сторонние инструменты. Во многих Java-проектах стала незаменимой библиотека, управляемая аннотациями, под названием «Lombok». Он сгенерирует байт-код для геттеров и сеттеров, но не добавит исходный код.
В Kotlin мы можем напрямую вызвать имя свойства, чтобы получить или установить значение:
Класс Person, определенный в строке 11, имеет первичный конструктор, который принимает один параметр — строку. Ключевое слово constructor может быть опущено при определенных условиях, но об этом позже.
Свойства в Kotlin делают то, что вы ожидаете, они убирают беспорядок и одновременно лаконичны и практичны.
Причина 4: Именованные аргументы и аргументы по умолчанию
Именованные аргументы — это то, что мне действительно нравится в языке. Я считаю, что это делает код более читабельным. Эта функция мне уже нравилась, когда я писал код на Objective-C, и она мне нравится до сих пор. Это просто то, что заставляет меня чувствовать себя хорошо, когда я смотрю на код.
Взгляните на этот пример, где я также ввожу значения по умолчанию.
Мы определили метод с пятью параметрами, из которых трем были присвоены значения по умолчанию. Это означает, что если вы опустите эти параметры вне вызова метода, вместо этого им будут присвоены значения по умолчанию.
Необходимо указать параметры, не имеющие значения по умолчанию. При использовании именованных аргументов их порядок не имеет значения, то есть вы можете его изменить. Взгляните на вызовы методов в строках 2 и 3, где мы поменяли порядок первых двух аргументов. Наличие именованных аргументов означает, что вам не нужно запоминать порядок аргументов, что снижает вашу когнитивную нагрузку.
Взгляните на этот пример и спросите себя, какой из этих двух вызовов метода имеет для вас наибольшее значение.
Ответ не очень сложный? Во втором вызове в строке 5 очевидно, какие значения вы передаете каким переменным. Даже если вы никогда не видели метод вычисления, вы хорошо понимаете, какие значения являются прошлыми и каково их значение. Добавьте к этому меньшую вероятность перепутать значения параметров, и я надеюсь, вы согласитесь, что именованные аргументы — это здорово.
Причина 5: классы данных
Лаконичность была одной из целей дизайна Kotlin, как упоминалось выше. Разработчики языка Kotlin действительно превзошли себя, когда создали классы данных. Взгляните на этот пример:
Кода в строке 10 достаточно для создания полноценного класса. Хотя он очень похож на класс, который мы определили в разделе Properties выше, он немного отличается. Мы добавили ключевое слово data перед ключевым словом class. Класс данных сгенерирует для нас:
- Свойства, определенные в определении класса
- Конструктор по умолчанию
- Методы установки и доступа для свойств
- equals() и его друг hashcode()
- метод toString()
- методы компонентаX()
- метод копирования()
Обратите внимание на метод копирования в строке 6. Вы можете указать столько свойств, сколько параметров, сколько хотите, что упрощает условное копирование.
Теперь вы можете сказать, что в Java есть что-то похожее с записями. И вы были бы правы. Но в то время как записи в Java неизменяемы, записи данных kotlin — нет. И это делает их идеальными для использования, например, в JPA.
Причина 6: функции расширения
Обычно конечные классы в Java и Kotlin не могут быть расширены. По сути, компилятор говорит: вот где это заканчивается, вы не можете добавлять больше функций в этот класс (запечатанные классы могут быть даже более ограниченными, как в Kotlin, так и в Java).
Однако функции расширения позволяют добавлять в класс любое количество новых функций, даже если класс является окончательным. Давайте посмотрим на пример:
Строки являются окончательными как в Java, так и в Котлине. Но в строке 8 мы определяем функцию расширения. Мы говорим, что tidyUp следует добавить как функцию к классу String. Мы вызываем метод в строке 4, и функция tidyUp получит ссылку на значения текущей String, input в этом примере. Таким образом, вы можете получить доступ ко всем доступным свойствам этого класса и управлять ими.
Хотя функции расширения не являются новым явлением (в C# и Ruby они уже давно есть), они, безусловно, новы для Java-разработчика. При правильном использовании они являются идеальным способом избавиться от всех этих вспомогательных и служебных классов, которые всегда лежат у нас под рукой.
Причина 7: сопрограммы
Технически сопрограммы не являются стандартной частью языка, поскольку различные типы реализации входят в свою собственную библиотеку и поэтому являются расширяемыми. Это означает, что в язык могут быть добавлены новые варианты. Но не будем забегать вперед. Давайте сначала определим, что такое сопрограммы.
Проще говоря, сопрограммы можно рассматривать как легкие потоки, часто называемые зелеными потоками, хотя технически они не являются полными.
Сопрограммы — это в основном конечные автоматы, которые могут быть приостановлены, когда встречаются блокирующие операции, такие как сетевой ввод-вывод, задержка (n) или операции с базой данных. Планировщик сопрограмм выберет следующую сопрограмму для выполнения до тех пор, пока она не завершится или не будет приостановлена.
Давайте рассмотрим пример простой сопрограммы:
Здесь мы создаем простую сопрограмму, которая после создания ждет одну секунду и выводит результат. Между тем блокировка основного потока из-за runBlocking.
Чтобы подчеркнуть, что сопрограммы действительно легкие, давайте создадим 100 000 сопрограмм и выполним их:
Обратите внимание, что обновление переменной i в области действия сопрограммы не является потокобезопасным. Здесь делается только простота. Лучшее решение смотрите здесь
Этот код выполняется на MacBook Pro конца 2013 года с 16 ГБ встроенной памяти за 742 мс. Это показывает, как мало ресурсов это требует.
Чтобы функции вызывались из сопрограмм, они должны иметь возможность приостанавливаться. Это достигается с помощью ключевого слова suspend. Взгляните на этот пример от JetBrains:
Определив эти функции как приостановленные, планировщик Kotlin имеет возможность приостановить их, если происходит операция блокировки.
Мы только затронули возможности сопрограмм в Kotlin, и есть много хороших руководств, которые дают вам более полное объяснение того, чего вы можете достичь с их помощью.
Справедливости ради надо отметить, что Java сама работает над зелеными потоками через Project Loom. Он использовал волокна вместо сопрограмм. Сборки раннего доступа можно найти здесь.
Краткое содержание
Kotlin предлагает множество улучшений по сравнению с классической Java, которые помогают повысить продуктивность и удовлетворенность разработчиков. По моему опыту, при представлении его команде разработчикам требовалось около пяти дней, чтобы ознакомиться и достичь того же уровня производительности, что и в Java. С этого момента они просто начнут развиваться быстрее и с большим удовольствием.
Почти 100-процентная совместимость с Java позволяет чрезвычайно легко смешивать его с вашей текущей разработкой Java, так почему бы не попробовать!