Если null-safety — главный аргумент «за» Kotlin, то работа с функциями и коллекциями — то, что делает повседневный код короче. Java-разработчику здесь знакомо многое (лямбды и Stream API появились ещё в Java 8), но Kotlin убирает церемонии: без функциональных интерфейсов, без .stream().collect(...), плюс две конструкции, которых в Java нет, — extension-функции и scope-функции.

Extension-функции: добавить метод чужому классу

fun String.toSlug(): String =
    lowercase().replace(' ', '-').filter { it.isLetterOrDigit() || it == '-' }

val slug = "Kotlin для Java разработчика".toSlug()

Класс String не менялся — extension-функция компилируется в обычный статический метод, но вызывается как метод объекта. Это убивает целый жанр Java-кода: классы StringUtils, DateUtils, CollectionUtils со статическими методами превращаются в функции, которые IDE подсказывает прямо на объекте.

Важно понимать границы: extension не влезает в класс (private-члены недоступны), не участвует в полиморфизме (выбирается по объявленному типу на этапе компиляции) и живёт в области видимости импорта — двум командам с разными расширениями одного класса они не мешают.

Лямбды без обёрток

val isEven: (Int) -> Boolean = { it % 2 == 0 }

fun retry(times: Int, action: () -> Unit) { /* ... */ }

retry(3) {
    sendEvent()
}

Тип функции пишется прямо: (Int) -> Boolean — не нужен Predicate, Function или собственный интерфейс. Единственный параметр лямбды доступен как it. И конвенция, которая делает Kotlin-код узнаваемым: если лямбда — последний аргумент, она выносится за скобки. Так retry(3) { ... } читается как встроенная языковая конструкция, хотя это обычная функция.

Scope-функции: let, apply, run, also

Пять маленьких функций для локальных преобразований. Реально нужны три:

// let — преобразовать значение или обработать не-null
val length = name?.let { it.trim().length }

// apply — настроить объект и вернуть его же
val props = Properties().apply {
    setProperty("user", "app")
    setProperty("ssl", "true")
}

// also — побочное действие «по дороге», объект вернётся как был
val order = createOrder().also { log.info("created {}", it.id) }

Ловушка — злоупотребление: вложенные let внутри apply внутри run превращают код в головоломку. Правило: одна scope-функция на выражение; сомневаетесь — пишите обычный if или локальную переменную.

Коллекции: read-only по умолчанию

val tags = listOf("kotlin", "jvm")          // List<String> — только чтение
val counts = mutableMapOf("a" to 1)         // изменяемая — по явному запросу

Интерфейсы разделены: List не имеет метода add, изменяемость — отдельный тип MutableList. Это то же «val по умолчанию», но для коллекций: функция, принимающая List, обещает его не менять.

Операции доступны прямо на коллекции, без .stream():

val names = users
    .filter { it.active }
    .sortedBy { it.createdAt }
    .map { it.name }

Каждый шаг здесь создаёт промежуточный список — для сотен элементов это неважно, для миллионов есть ленивые sequences: users.asSequence().filter{...}.map{...}.toList() обрабатывает элементы по одному, как Stream в Java. Правило: начинайте с обычных коллекций, sequence — когда цепочка длинная и данных много.

Полезная мелочь: mapNotNull — map и отбрасывание null одним шагом, идеально в паре с nullable-типами.

Коротко

  • Extension-функции добавляют методы чужим классам без наследования: конец эпохи Utils-классов; но это статика — без полиморфизма и доступа к private.
  • Типы функций пишутся напрямую ((Int) -> Boolean), лямбда последним аргументом выносится за скобки.
  • Scope-функции: let — преобразование/не-null, apply — настройка объекта, also — побочное действие; больше одной на выражение — уже перебор.
  • Коллекции read-only по умолчанию, изменяемость — отдельные Mutable-типы; map/filter — прямо на коллекции.
  • Для больших данных и длинных цепочек — asSequence(): ленивость как у Stream.

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

  • Корутины и virtual threads — следующий шаг маршрута.
  • Лямбды и Stream API в Java — функциональная Java для сравнения.
  • Коллекции Java — устройство ArrayList и HashMap: в Kotlin под капотом те же классы.