Первые часы с Kotlin — сплошные «а, так можно было»: меньше точек с запятой, меньше типов, меньше повторов. Но за мелочами стоят два системных сдвига: почти всё в Kotlin — выражение, а не инструкция, и неизменяемость — выбор по умолчанию. Разберём базу, которой хватит, чтобы читать чужой код.

val и var: неизменяемость по умолчанию

val name = "Order-42"   // неизменяемая ссылка, аналог final
var retries = 0         // изменяемая
retries++               // ок
// name = "Other"       // не компилируется

Тип выводится из значения — писать String name не нужно, но можно: val name: String = "Order-42". Правило стиля простое: везде val, var — только когда без него не обойтись. В Java final у локальных переменных пишут редко, потому что многословно; в Kotlin неизменяемость короче изменяемости — и оттого встречается повсеместно.

Функции: от инструкций к выражениям

fun grade(percent: Int): String {
    return if (percent >= 90) "отлично" else "нормально"
}

fun grade2(percent: Int) = if (percent >= 90) "отлично" else "нормально"

Здесь два новшества сразу. Первое: if — выражение, оно возвращает значение, поэтому тернарный оператор в Kotlin не нужен — его роль играет обычный if/else. Второе: функцию из одного выражения можно записать через = без фигурных скобок и return.

Функции живут и вне классов — на верхнем уровне файла. Утилитный класс со статическими методами превращается в файл с функциями.

when: switch, который дорос

val label = when (status) {
    "NEW", "PENDING" -> "в работе"
    "DONE" -> "готово"
    else -> "неизвестно"
}

when — тоже выражение. Ветки не «проваливаются» (никакого break), можно матчить по диапазонам (in 1..9), типам (is String) и произвольным условиям (when { x > 0 -> ... }). В связке с sealed-классами компилятор проверяет полноту веток — об этом в статье про классы.

Шаблоны строк

val msg = "Заказ $orderId: ${items.size} позиций"

Вместо конкатенации или String.format — подстановка прямо в строке: $имя для переменной, ${выражение} для выражения. Многострочные строки — в тройных кавычках """...""", без экранирования.

Именованные аргументы и значения по умолчанию

fun connect(host: String, port: Int = 5432, ssl: Boolean = false) { /* ... */ }

connect("db.local")                        // порт и ssl — по умолчанию
connect("db.local", ssl = true)            // именованный аргумент

Это закрывает две больные темы Java разом: телескопические перегрузки (пять версий метода с разным числом параметров) и билдеры ради читаемости вызова. Один метод со значениями по умолчанию заменяет всю лестницу перегрузок.

Мелочи, которые встретятся сразу

  • Точка с запятой не нужна (можно, но линтер осудит).
  • new нет: val user = User("Anna").
  • Приведение типов — через as, проверка — через is; после успешной проверки is компилятор сам считает тип уточнённым (smart cast) — явный каст не нужен.
  • Пакеты и импорты — как в Java; имя файла не обязано совпадать с именем класса.
  • Unit — аналог void, но полноценный тип; в сигнатурах его обычно опускают.

Коротко

  • val — неизменяемая ссылка, var — изменяемая; правило: везде val, var — по необходимости.
  • if и when — выражения: возвращают значение, тернарный оператор не нужен, ветки when не проваливаются.
  • Функции из одного выражения пишутся через =; функции могут жить вне классов.
  • Шаблоны строк $x и ${expr} заменяют конкатенацию; именованные аргументы и значения по умолчанию — телескопические перегрузки и билдеры.
  • Smart cast: после is-проверки тип уточняется автоматически.

Что почитать дальше

  • Null-safety: типы против NPE — главное отличие системы типов.
  • Классы: data, sealed, object — куда делись геттеры и сеттеры.
  • Функции и коллекции — лямбды и extension-функции.