Если 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 под капотом те же классы.