Основы Kotlin
Повседневный синтаксис Kotlin выглядит как облегчённая Java — и именно поэтому его читают неверно. За каждой «просто удобной» конструкцией стоит конкретное решение компилятора, и вопрос на собеседовании всегда про него. val фиксирует не объект, а ссылку, и это разные вещи. if и when — не инструкции, а выражения со значением, откуда сразу следует и отсутствие тернарного оператора, и требование исчерпывающего when в позиции выражения. Строковый шаблон не интерпретируется в рантайме — он разворачивается компилятором в конкатенацию, так что «дорогой» он только на вид.
Второй пласт темы — про типы, которых в Java просто нет. Unit — не void: это настоящий тип ровно с одним значением, и именно поэтому лямбда без результата имеет тип () -> Unit, тогда как void в аргументы дженерика подставить нельзя. Any — корень non-nullable части иерархии (настоящая вершина — Any?), и, вопреки популярному ответу, wait/notify его членами не являются. Третий пласт — то, что компилятор дописывает за вас: функция верхнего уровня превращается в static-метод класса ИмяФайлаKt, а аргумент по умолчанию — в синтетический $default-мост с битовой маской переданных параметров (отсюда и @JvmOverloads для Java). Наконец, scope-функции: их пять, и различаются они всего по двум осям — this против it и «вернуть получателя» против «вернуть результат лямбды». Разбор по слоям — ниже.
Карта темы
- val, var и границы неизменяемости — почему
valзамораживает ссылку, а не объект, что делает backing field и какvalс собственным getter возвращает разное значение при каждом чтении. - if, when и try как выражения — значением становится последнее выражение выбранной ветки, откуда берутся отсутствие тернарного оператора и требование исчерпывающего
when. - Строковые шаблоны —
$nameи${expr}, разворачивание в конкатенацию на этапе компиляции, raw-строки сtrimIndent()и экранирование литерального доллара. - Тип Unit — тип с ровно одним значением против
void, который типом не является, и почему это важно для дженериков и лямбд. - Иерархия Any и Nothing —
Anyкак корень non-nullable типов,Any?как настоящая вершина,Nothingкак дно и что на самом деле объявлено уAny. - Функции верхнего уровня — функция без класса-держателя, генерируемый класс
ИмяФайлаKtсо static-членами и переименование через@JvmName. - Аргументы по умолчанию и именованные аргументы — умолчание как выражение, вычисляемое на каждом вызове, синтетический
$default-мост с битовой маской и@JvmOverloadsдля Java. - Scope-функции — две оси выбора (
thisпротивit, получатель против результата лямбды), роль каждой из пяти и цена вложенности.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Считать, что val делает неизменяемым сам объект, а не только ссылку | Объект за ссылкой изменяем: val с MutableList спокойно принимает add |
Кэшировать значение val с собственным getter | Getter пересчитывается при каждом чтении — два чтения законно дают разное |
Путать val с константой времени компиляции | Это const val: он встраивается в место использования и допустим только для примитивов и String |
| Искать в Kotlin тернарный оператор | Его нет и не будет: эту роль полностью закрывает if в позиции выражения |
Забывать, что when в позиции выражения обязан быть исчерпывающим | Код не компилируется, пока не добавлен else или не покрыты все варианты sealed/enum |
Считать, что when принимает только константы, как switch | when без субъекта цепляет произвольные условия, in-диапазоны и is-проверки со smart cast |
Думать, что строковый шаблон медленнее + | Компилятор разворачивает его в ту же конкатенацию — цена ровно та же |
Забывать ${} вокруг выражения | Подставится только первое имя: "$user.name" даст User(...) и затем литеральное .name |
| Не экранировать литеральный знак доллара | Компилятор читает $ как начало плейсхолдера и падает на неизвестном имени |
Считать Unit ключевым словом вроде void | Unit — тип с ровно одним значением; он годится в аргумент дженерика, а void — нет |
Путать Unit с Nothing | У Unit одно значение, у Nothing — ни одного: функция с типом Nothing не возвращается вовсе |
Ожидать у Any методы wait, notify и getClass | У Any объявлены только equals, hashCode и toString |
Считать, что переменная типа Any может хранить null | Вершина иерархии — Any?, а не Any; Any — корень только non-nullable типов |
| Считать, что для функции верхнего уровня JVM-класс не генерируется | Генерируется класс ИмяФайлаKt со static-членами — из Java это UtilsKt.foo() |
| Переносить привычку Python «умолчание вычисляется один раз при определении» | В Kotlin умолчание — выражение, вычисляемое заново на каждом вызове с опущенным аргументом |
Ожидать, что Java увидит умолчания без @JvmOverloads | Из Java видна полная сигнатура плюс синтетический $default-мост — перегрузок нет |
Брать apply, когда обратно нужно вычисленное значение лямбды | apply возвращает получателя; результат лямбды молча отбрасывается |
| Оценивать вложенность scope-функций по цене аллокаций | Все пять — inline, лямбда не аллоцируется: платите не скоростью, а читаемостью — it перекрывает it |
Значение для собеседований
Основы — это фильтр первого прохода. По ответу на «в чём разница между val и var» интервьюер за одну фразу понимает, думает ли кандидат о ссылке или об объекте: «val — это неизменяемость» — стоп-слово, «val — это ссылка, присваиваемая один раз, объект за ней изменяем» — зелёный свет. Дальше та же логика применяется ко всей теме: спрашивают не термины, а модель исполнения — что компилятор физически кладёт в байт-код, когда встречает шаблон строки, умолчание в сигнатуре или функцию, объявленную вне класса.
Что обычно проверяют:
valпротивvarи точную границу гарантииval(ссылка, а не объект; собственный getter).- Почему
ifиwhen— выражения, почему нет тернарного оператора и когдаwhenобязан быть исчерпывающим. - Во что компилятор разворачивает строковый шаблон и как вставить литеральный
$. UnitпротивvoidиUnitпротивNothing.- Где стоят
Any,Any?иNothingи что реально объявлено уAny. - Во что компилируется функция верхнего уровня и как её вызвать из Java.
- Когда вычисляется аргумент по умолчанию и что генерирует
@JvmOverloads. - Две оси выбора scope-функции и почему вложенность вредна.
Типичный неверный ответ: «val — это иммутабельность, var — мутабельность». Отсюда кандидат уверенно выводит следующее: раз val неизменяем, то val с MutableList не примет add, а два чтения val обязаны дать одинаковый результат. Оба вывода ложны. val запрещает переприсваивание ссылки и ровно ничего не говорит про объект: val items = mutableListOf<String>(); items.add("x") компилируется и работает. А val с собственным getter вообще не имеет backing field и пересчитывается при каждом обращении — именно поэтому такое свойство не годится и для smart cast. Второй классический провал — «умолчание mutableListOf() в сигнатуре создаст один общий список»: это правило Python, а не Kotlin; в Kotlin умолчание вычисляется заново при каждом вызове, где аргумент опущен.