Kotlin (Ко?тлин) — статически типизированный язык программирования, работающий поверх JVM и разрабатываемый компанией JetBrains. Kotlin сочетает в себе принципы объектно-ориентированного и функционального языка программирования. По заявлению разработчиков, обладает такими качествами, как прагматичность, лаконичность и интероперабельность (pragmatic, concise, interoperable). Программы, написанные на нём, могут выполняться на JVM или компилироваться в JavaScript, не за горами поддержка native компиляции. Важно отметить, язык создавался одновременно с инструментами разработки и был изначально заточен под них.
К настоящему времени Котлину посвящено немало статей и сделано немало докладов. Мы постараемся сосредоточиться не столько на отличительных особенностях языка, хвалить его или ругать, сколько на нашем практическом опыте использования заявленных преимуществ.
Итак, обо всем по порядку…
Лучший tooling
Разработчиком языка Kotlin является компания JetBrains, разработавшая пожалуй лучшую IDE для Java и многих других языков программирования. Несмотря на всю многословность языка Java, скорость написания остается очень высокой: среда “пишет код” за вас.
С Kotlin складывается ощущение, что вы купили новую клавиатуру и все никак не можете привыкнуть к ней и печатать вслепую не получается. IntelliSense зачастую просто не успевает за скоростью набора текста, там где для Java IDE сгенерирует целый класс, для Kotlin вы будете смотреть на прогресс бар. И проблема не только для новых файлов: при активной навигации по проекту IDE просто начинает зависать и спасает только ее перезапуск.
Огорчает, что многие трюки, к которым вы привыкли, просто перестают работать. К примеру, Live Templates. Android Studio — (Версия IntelliJ IDEA заточенная под Android разработку) поставляется вместе с набором удобных шаблонов для часто используемых операций, таких как логгирование. Комбинация logm+ Tab вставит за вас код, который напишет в лог сообщение о том, какой метод и с какими параметрами был вызван:
Log.d(TAG, "method() called with: param = [" + param + "]");
При этом данный шаблон “умеет” правильно определять метод и параметры, в зависимости от того, где вы его применили.
Однако для Kotlin это не работает, более того, вам придется заводить отдельный шаблон (например klogd + Tab) для Kotlin и использовать его в зависимости от языка программирования. Причина, почему для языков, которые стопроцентно совместимы IDE, приходится выставлять настройки дважды, остается для нас загадкой.
Легкость в освоении
Kotlin, несмотря на возможность компиляции в JavaScript и потенциально в нативный код (используя Kotlin.Native), в первую очередь, язык для JVM и нацелен на то, чтобы избавить Java разработчиков от ненужного, потенциально опасного (в смысле привнесения багов) бойлерплейта. Однако ошибочно считать, что вы с первых строк на Kotlin будете писать на Kotlin. Если проводить аналогию с языками, то поначалу вы будете писать на “рунглише” с сильным Java акцентом. Данный эффект подтвержден ревью своего кода, спустя какое-то время, а также наблюдением за кодом коллег, только начинающих освоение языка. Наиболее заметно это проявляется в работе с null и nonNull типами, а также излишней “многословности” выражений — привычки, с которой бороться сложнее всего. Кроме того, наличие просто огромного числа новых возможностей вроде extension-методов открывают “Ящик Пандоры” для написания черной магии, добавляя лишнюю сложность там, где этого не нужно, а также делая код более запутанным, т.е. менее приспособленным для ревью. Чего только стоит перегрузка метода invoke() [подробнее], которая позволяет замаскировать его вызов под вызов конструктора так, что визуально создавая объект типа Dog получаете все что угодно:
class Dog private constructor() {
companion object {
operator fun invoke(): String = "MAGIC"
}
}
object DogPrinter {
@JvmStatic
fun main(args: Array<String>) {
println(Dog()) // MAGIC
}
Таким образом, несмотря на то, что на освоение синтаксиса уйдет не более недели, на то, чтобы научиться правильно применять фичи языка, может уйти не один месяц. Местами потребуется более детальное изучение принципов работы того или иного синтаксического сахара, включая изучения полученного байт-кода. При использовании Java, вы сможете всегда обратиться к источникам вроде Effective Java для того, чтобы избежать многих неприятностей. Несмотря на то, что Kotlin проектировался с учетом “неприятностей”, привнесенных Java, о “неприятностях”, привнесенных Kotlin еще только предстоит узнать.
Null safety
Язык Kotlin обладает изящной системой типов. Она позволяет в большинстве случаев избежать самую популярную в Java проблему — NullPointerException. Каждый тип имеет два варианта в зависимости от того, может ли переменная этого типа принимать значение null. Если переменной можно присвоить null, к типу добавляется символ вопроса. Пример:
val nullable: String? = null
val notNull: String = ""
Методы nullable переменной вызываются с использованием оператора .? Если такой метод вызван на переменной, имеющей значение null, результат всего выражения тоже примет значение null, при этом метод вызван не будет и NullPointerException не случится. Конечно, разработчиками языка оставлен способ вызвать метод на nullable переменной, не смотря ни на что, и получить NullPointerException. Для этого вместо? придётся написать !!:
nullable!!.subSequence(start, end)
Эта строчка сразу режет глаз и делает код менее уютным. Два идущих подряд восклицательных знака повышают вероятность, что такой код будет написан исключительно осознанно. Впрочем, сложно придумать ситуацию, в которой понадобилось бы использовать оператор !!..
Все выглядит хорошо до тех пор, пока весь код написан на Котлине. Если же Котлин используется в существующем проекте на Java, всё становится гораздо сложнее. Компилятор не может отследить, в каких переменных к нам придёт null, и, соответственно, верно определить тип. Для переменных, пришедших из Java проверки на null на момент компиляции отсутствуют. Ответственность за выбор правильного типа ложится на разработчика. При этом чтобы корректно работала автоматическая конвертация из Java в Kotlin, в коде на Java должны быть проставлены @Nullable/@Nonnull аннотации. Полный список поддерживаемых аннотаций можно найти по ссылке.
Если же null из Java кода пробрался в Котлин, произойдёт крэш с исключением следующего вида:
FATAL EXCEPTION: main
Process: com.devexperts.dxmobile.global, PID: 16773
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.devexperts.dxmobile.global/com.devexperts.dxmarket.client.ui.generic.activity.GlbSideNavigationActivity}: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter savedInstanceState
Дизассемблировав байт-код, находим место, откуда было брошено исключение:
ALOAD 1
LDC "param"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull(Ljava/lang/Object;Ljava/lang/String;)V
Дело в том, что для всех параметров не-private методов компилятор добавляет специальную проверку: вызывается метод стандартной библиотеки
kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(param, "param")
При желании его можно отключить при помощи директивы компилятора
-Xno-param-assertions
Использовать эту директиву стоит лишь в крайнем случае, ведь она дает лишь незначительный прирост производительности в ущерб вероятной потере надежности.
Ко всем классам, имеющим метод get(), в Котлине можно применять оператор []. Это очень удобно. Например:
val str = “my string”
val ch = str[2]
Однако оператор доступа по индексу можно применять только для non-null типов. Nullable версии не существует, и в таком случае придётся явно вызвать метод get():
var str: String? = null
val ch = str?.get(2)
Properties
Котлин упрощает работу с полями классов. Обращаться к полям можно, как к обычным переменным, при этом будет вызван геттер или сеттер нужного поля.
// Java code
public class IndicationViewController extends ViewController {
private IndicationHelper indicationHelper;
protected IndicationHelper getIndicationHelper() {
return indicationHelper;
}
}
// Kotlin code
val indicationViewController = IndicationViewController()
val indicationHelper = indicationViewController.indicationHelper
Все усложняется, если требуется переопределить геттер Java класса в классе на Котлине. На первый взгляд кажется, что indicationHelper — это полноценное property, совместимое с Котлином. На самом деле это не так. Если мы попробуем переопределить его “в лоб”, получим ошибку компиляции:
class GlobalAccountInfoViewController(context: Context) : IndicationViewController(context) {
protected open val indicationHelper = IndicationHelper(context, this)
}
Всё сделано правильно: в классе наследнике объявлено проперти, геттер которого имеет абсолютно идентичную сигнатуру геттеру суперкласса. Что же не так? Компилятор заботится о нас и считает, что переопределение произошло по ошибке. На эту тему существует даже обсужнение на форуме Котлина. Отсюда мы узнаём две важные вещи:
- “Java getters are not seen as property accessors from Kotlin” — Геттеры в Java коде не видны из Котлина как проперти.
- “This may be enhanced in the future, though” — есть надежда, что в будущем это изменится к лучшему.
Там же приводится единственный правильный способ всё-таки добиться нашей цели: создать private-переменную и одновременно с этим переопределить геттер.
class GlobalAccountInfoViewController(context: Context) : IndicationViewController(context) {
private val indicationHelper = IndicationHelper(context, this)
override fun getIndicationHelper() = indicationHelper
}
100% Java-interop
Пожалуй, стоило поставить этот пункт на первое место, поскольку именно Java-interop позволил новому языку так быстро набрать такую популярность, что даже Google заявил об официальной поддержке языка для разработки под Android. К сожалению, здесь также не обошлось без сюрпризов.
Рассмотрим такую простую и известную всем Java разработчикам вещь, как модификаторы доступа или модификаторы видимости. В Java их четыре штуки: public, private, protected и package-private. Package-private используется по умолчанию, если вы не указали иного. В Kotlin по умолчанию используется модификатор public, и он, как и protected и private, называется и работает точно так же, как в Java. А вот модификатор package-private в Kotlin называется internal и работает он несколько иначе.
Дизайнеры языка хотели решить проблему с потенциальной возможностью нарушить инкапсуляцию при применении package-private модификатора путем создания в клиентском коде пакета с тем же именем, что и в библиотечном коде и предопределении нужного метода. Такой трюк часто используется при написании unit-тестов для того, чтобы не открывать “наружу” метод только для нужд тестирования. Так появился модификатор internal, который делает объект видимым внутри модуля.
Модулем называется:
- Модуль в проекте IntelliJ Idea
- Проект в Maven
- Source set в Gradle
- Набор исходников скомпилированных одним запуском ant-скрипта
Проблема в том, что на самом деле internal это public final. Таким образом при компиляции на уровне байт кода может так получиться, что вы случайно переопределите метод, который переопределять не хотели. Из-за этого компилятор переименует ваш метод, чтобы такого не произошло, что в свою очередь сделает невозможным вызов данного метода из Java кода. Даже если файл с этим кодом будет находится в том же модуле, в том же пакете.
class SomeClass {
internal fun someMethod() {
println("")
}
}
public final someMethod$production_sources_for_module_test()V
Вы можете скомпилировать ваш Kotlin код с internal модификатором и добавить его как зависимость в ваш Java проект, в таком случае вы сможете вызвать этот метод там, где protected модификатор вам бы этого сделать не дал, т.е получите доступ к приватному API вне пакета (т.к метод де-факто public), хотя и не сможете переопределить. Складывается ощущение, что модификатор internal был задуман не как часть “Прагматичного языка”, а скорее как фича IDE. При том, что сделать такое поведение можно было, например, при помощи аннотаций. На фоне заявлений о том, что в Kotlin очень мало ключевых слов зарезервировано, например, для корутин, internal фактически прибивает гвоздями ваш проект на Kotlin к IDE от JetBrains. Если вы разрабатываете сложный проект, состоящий из большого числа модулей, часть из которых могут использоваться как зависимость коллегами, в проекте на чистой Java, хорошо подумайте о том, стоит ли писать общие части на Kotlin.
Data Classes
Следующая, пожалуй одна из самых известных фич языка — data классы. Data классы позволяют вам быстро и просто писать POJO-объекты, equals, hashCode, toString и прочие методы для которых компилятор напишет за вас.
Это действительно удобная вещь, однако, ловушки могут поджидать вас в совместимости с используемыми в проекте библиотеками. В одном из наших проектов мы использовали Jackson для сериализации/десериализации JSON. В тот момент, когда мы решили переписать некоторые POJO на Kotlin, оказалось что аннотации Jackson некорректно работают с Kotlin и необходимо дополнительно подключать отдельный jackson-module-kotlin модуль для совместимости.
И в заключение
Подводя итоги, хотелось бы сказать что несмотря на то, что статья, возможно, покажется вам критикующей Kotlin, нам он нравится! Особенно, на Android, где Java застряла на версии 1.6 — это стало настоящим спасением. Мы понимаем, что Kotlin.Native, корутины и прочие новые возможности языка — это очень важные и правильные вещи, однако, они пригодятся далеко не всем. В то время, как поддержка IDE — это то, чем пользуется каждый разработчик, и медленная работа IDE нивелирует всю выгоду в скорости от перехода с многословной Java на Kotlin. Переходить ли на новый Kotlin или пока остаться на Java — выбор каждой отдельной команды, мы лишь хотели поделиться проблемами, с которыми нам пришлось столкнуться, в надежде на то, что кому-то это может сберечь время.
Авторы:
Тимур Валеев, инженер-программист Devexperts
Александр Верещагин, инженер-программист Devexperts
Комментарии (15)
solver
01.09.2017 20:30+10И где в статье «опыт боевого применения»?
vlanko
02.09.2017 13:13+1Ну, написали, что нужно несколько месяцев переучивать. Видать, еще ничего не накодили хорошего.
alever
03.09.2017 01:32+2По-моему в статье как раз и описываются возможные проблемы, с которыми может столкнуться человек, решивший писать на Kotlin. Разве нет? А вы о чём ожидали прочитать в статье с таким названием?
nerumb
01.09.2017 21:37аннотации Jackson некорректно работают с Kotlin и необходимо дополнительно подключать отдельный jackson-module-kotlin модуль для совместимости
Можно и без jackson-module-kotlin, только тогда чисто Kotlin специфичные вещи работать не будут, что вполне логично.
… хорошо подумайте о том, стоит ли писать общие части на Kotlin.
Вы серьезно? Основное, чем вы аргументируете свое заключение, это internal. Который и не так часто используется.
vlsinitsyn
02.09.2017 01:38-1Вот очередная статья из серии «hello word» на котлин, которые регулярно появляются на хабре не первый год.
Когда уже можно будет почитать на хабре уже и что нибудь более серьезное? -Написать хелло ворд уже все успели.
Хотелось бы более критической, профессиональной и независимой оценки, что в котлине хорошо, что — не очень. Идеальных языков не бывает. А щенячий восторг статей намекает на недостаток опыта и профессионализма у авторов.
Вот как живет котлин в больших проектах, какие фремворки используются, библиотеки?
Общие утверждения типа «все как и в джава» бесполезны, т.к. во-первых для этого надо знать java, а во-вторых, там далеко не все так очевидно, как утверждают всякие «популяризаторы» котлина, весь опыт которых заключается в прочтении базовой документации по языку.DragonFire
02.09.2017 14:31+1У нас райдер на котлин написан =) Та часть, которая работает поверх идеи, т.е. фронтенд. Там много кода.
werewolfspb
03.09.2017 11:19Так вы бы поделились с публикой о подводных камнях непосредственно от авторов. Я думаю, все бы с удовольствием почитали! :)
DragonFire
03.09.2017 11:54Лично мне нечем делиться. Оно просто работает =) Тем более я учил котлин не после java а после c#.
Находили пару раз баги в компиляторе, также репортили их в команду котлина, так же ждали следующего минорного релиза с фиксами… Но последнее время проблем нету у нас.
lany
02.09.2017 04:57+3Вот, трезвый взгляд на ситуацию. Я тоже постоянно натыкаюсь на недоработки и баги тулинга. Причём довольно сложно делать какую-то фичу IDE так, чтобы она волшебным образом заработала и для Джавы, и для Котлина сразу. Иногда что-то можно объединить, но в большинстве случаев это двойная работа для авторов IDE. В джаве больше всяких инспекцией и квик-фиксов, в том числе ориентированных не на фичи языка, а на библиотеки. Логично ожидать, что при использовании тех же библиотек из Котлина вы получите аналогичную поддержку. Но так бывает увы не всегда.
lany
02.09.2017 04:59А про штуки вроде разных имён для live-темплейтов — лучше посылать баг-репорт в youtrack.jetbrains.com.
Blekel
02.09.2017 13:29Вот некоторые люди несколько лет Kotlin используют, но статей зря не пишут. А вам не советую генерить информационный шум с поводом проснувшегося человека
cynovg
Не официальный язык, а язык, получивший поддержку, помимо Java и C++.
alever
Отсюда:
Официальный. Да, наряду с Java и С++, но в статье нигде и не говорится, что единственный официальный.
Pilat
А что такое официальный? Я смотрю Firebase — Java есть, Kotlin нет. Надо писать на Kotlin используя вставка на яве?
alever
У нас в проекте тоже используется Firebase. Подключаете его в Gradle-скрипте и спокойно используете из Kotlin-классов.