Базовые конструкции

Типизация

Неявная и явная типизация

Kotlin поддерживает автоматический вывод типов (type inference), что позволяет не указывать тип явно, если компилятор может его определить из контекста.

// Неявная типизация - компилятор выводит тип сам
val name = "John"           // String
val age = 25               // Int
val price = 19.99          // Double
val isActive = true        // Boolean

// Явная типизация - указываем тип явно
val name: String = "John"
val age: Int = 25
val price: Double = 19.99
val isActive: Boolean = true

Ключевые различия:

  • val - неизменяемая переменная (immutable), аналог final в Java
  • var - изменяемая переменная (mutable)

Val vs Var и типы переменных

// val - значение не может быть переассигнено
val immutableValue = 10
// immutableValue = 20  // ОШИБКА - val не может быть переассигнен

// var - значение может быть переассигнено
var mutableValue = 10
mutableValue = 20  // OK

// val НЕ гарантирует неизменяемость объекта
val list = mutableListOf(1, 2, 3)
list.add(4)  // OK - содержимое можно изменять
// list = mutableListOf()  // ОШИБКА - переассигнение запрещено

// const val - compile-time константы, только для примитивов и String
const val MAX_SIZE = 100  // должна быть на уровне класса или глобально

Лучшая практика: Используйте val по умолчанию, var только когда необходимо.

String Templates (шаблоны строк)

val name = "Alice"
val age = 30

// Простая интерполяция
val greeting = "Hello, $name!"  // "Hello, Alice!"

// Выражения в шаблонах
val message = "Age: ${age + 1}"  // "Age: 31"
val formatted = "Name: ${name.uppercase()}"  // "Name: ALICE"

// Многострочные строки
val multiline = """
    Line 1
    Line 2
    Line 3
""".trimIndent()

// С переменными в многострочных строках
val template = """
    User: $name
    Age: $age
""".trimIndent()

// Экранирование
val path = "C:\\Users\\$name"  // нужно экранировать \
val escaped = "Escape: \$name"  // \$ выводит литерально $

Nullable типы и операторы безопасности

Nullable типы (?) - основа null-safety в Kotlin. По умолчанию все типы non-null.

var name: String = "John"        // Не может быть null
var nullable: String? = null     // Может быть null

// Безопасный вызов (?.) - вернет null если объект null
val length = nullable?.length    // Int? или null

// Elvis-оператор (?:) - возвращает правое значение если левое null
val len = nullable?.length ?: 0  // Int (ненулевое значение)

// Not-null assertion (!!) - принудительное приведение, может бросить NPE
val forcedLength = nullable!!.length  // NPE если nullable == null

Scope Functions (базовое введение)

// let - выполняет блок кода если объект не null
nullable?.let { 
    println("Значение: $it")  // it - ссылка на объект
}

// run - выполняет блок кода в контексте объекта
val result = "Hello".run {
    this.uppercase()  // this - ссылка на объект
}

// also - выполняет действие с объектом, возвращает сам объект
val list = mutableListOf<String>().also {
    it.add("item1")
    it.add("item2")
}

// apply - конфигурирует объект, возвращает сам объект
val person = Person().apply {
    name = "John"
    age = 30
}

Управляющие конструкции

When - мощная замена switch

when в Kotlin - это выражение (возвращает значение), не просто оператор.

// Простое использование
val result = when (x) {
    1 -> "один"
    2, 3 -> "два или три"
    in 4..10 -> "от четырех до десяти"
    is String -> "это строка"
    else -> "что-то другое"
}

// Без аргумента - как цепочка if-else
val message = when {
    age < 18 -> "несовершеннолетний"
    age < 65 -> "взрослый"
    else -> "пенсионер"
}

// When как выражение с возвратом
val status = when (code) {
    200 -> {
        log("Success")
        "OK"
    }
    404 -> {
        log("Not found")
        "ERROR"
    }
    else -> "UNKNOWN"
}

If как выражение

В Kotlin if может возвращать значение, что делает тернарный оператор ненужным.

val max = if (a > b) a else b
val status = if (score >= 60) "сдал" else "не сдал"

// if как выражение с блоками
val categorized = if (value > 0) {
    println("Positive")
    "positive"
} else if (value < 0) {
    println("Negative")
    "negative"
} else {
    "zero"
}

Циклы for и while

// For по диапазону
for (i in 1..5) print(i)        // 1, 2, 3, 4, 5
for (i in 1 until 5) print(i)   // 1, 2, 3, 4
for (i in 5 downTo 1) print(i)  // 5, 4, 3, 2, 1
for (i in 1..10 step 2) print(i) // 1, 3, 5, 7, 9

// For по коллекции
for (item in list) println(item)
for ((index, value) in list.withIndex()) {
    println("$index: $value")
}

// While остается классическим
while (condition) { /* код */ }
do { /* код */ } while (condition)

Ranges и коллекции

Ranges (диапазоны)

Ranges представляют последовательность значений между двумя точками.

val range1 = 1..10           // IntRange от 1 до 10 включительно
val range2 = 1 until 10      // от 1 до 9 (10 не включается)
val range3 = 10 downTo 1     // от 10 до 1 по убыванию
val range4 = 1..10 step 2    // 1, 3, 5, 7, 9

// Проверка принадлежности
if (5 in 1..10) println("5 в диапазоне")
if (x !in 1..10) println("x вне диапазона")

// Других типов
val charRange = 'a'..'z'
val longRange = 1L..100L

Type Aliases

// Создание алиасов для сложных типов
typealias Predicate<T> = (T) -> Boolean
typealias NumberConverter = (String) -> Int?

// Использование
val isPositive: Predicate<Int> = { it > 0 }
val stringToInt: NumberConverter = { it.toIntOrNull() }

// В функциях
fun filter(items: List<Int>, predicate: Predicate<Int>): List<Int> {
    return items.filter(predicate)
}

Коллекции

Kotlin различает изменяемые и неизменяемые коллекции.

// Неизменяемые коллекции (read-only)
val list = listOf(1, 2, 3)
val set = setOf("a", "b", "c")
val map = mapOf("key1" to "value1", "key2" to "value2")

// Изменяемые коллекции (mutable)
val mutableList = mutableListOf(1, 2, 3)
val mutableSet = mutableSetOf("a", "b")
val mutableMap = mutableMapOf("key1" to "value1")

// Полезные операции
val filtered = list.filter { it > 1 }      // [2, 3]
val mapped = list.map { it * 2 }           // [2, 4, 6]
val sum = list.sum()                       // 6
val first = list.first()                   // 1
val last = list.last()                     // 3

Деструктуризация

Позволяет извлекать компоненты из объектов в отдельные переменные.

// Деструктуризация пар и тройок
val pair = "John" to 25
val (name, age) = pair

// Деструктуризация data classes
data class Person(val name: String, val age: Int)
val person = Person("John", 25)
val (personName, personAge) = person

// В циклах
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) {
    println("$key = $value")
}

// Игнорирование значений с помощью _
val (name, _) = person  // возраст игнорируется

Smart-cast и проверки типов

Проверка типов (is/as)

is - проверяет тип объекта, as - приводит к типу.

// is - проверка типа (аналог instanceof в Java)
if (obj is String) {
    println(obj.length)  // smart-cast к String
}

// !is - проверка на НЕ принадлежность типу
if (obj !is String) return

// as - небезопасное приведение типа
val str = obj as String

// as? - безопасное приведение, вернет null если не удается
val str = obj as? String

Smart-cast

Компилятор автоматически приводит тип после проверки is.

fun processValue(value: Any) {
    if (value is String) {
        // После проверки is, value автоматически имеет тип String
        println(value.length)        // не нужно явное приведение
        println(value.uppercase())
    }
    
    if (value is Int && value > 0) {
        // Smart-cast работает с логическими операторами
        println(value * 2)
    }
}

// Smart-cast с nullable типами
fun processNullable(str: String?) {
    if (str != null) {
        // После проверки на null, str имеет тип String
        println(str.length)
    }
}

Enum классы

enum class Status {
    ACTIVE, INACTIVE, PENDING
}

// С параметрами
enum class Priority(val level: Int) {
    LOW(1),
    MEDIUM(5),
    HIGH(10);
    
    fun isUrgent() = level > 7
}

// Встроенные методы
Priority.valueOf("HIGH")  // Получить по имени
Priority.values()         // Все значения
Priority.HIGH.ordinal     // Порядковый номер (2)

// Использование в when
fun processPriority(priority: Priority) = when (priority) {
    Priority.LOW -> "Can wait"
    Priority.MEDIUM -> "Do soon"
    Priority.HIGH -> "Do now"
}

Важные моменты для собеседования:

  • Smart-cast работает только с val или локальными переменными
  • Не работает с var свойствами классов (они могут изменяться в других потоках)
  • Проверка is более предпочтительна чем as для безопасности типов
  • Kotlin's null-safety устраняет NPE на этапе компиляции
  • when более мощный чем switch в Java - может работать с любыми типами и условиями
  • String templates значительно улучшают читаемость кода
  • Type aliases полезны для улучшения читаемости сложных типов

Классы и функции

Специализированные классы

Data class

Data class - автоматически генерирует equals(), hashCode(), toString(), copy() и componentN() функции. Идеален для DTO, entities, value objects.

data class User(val id: Long, val name: String, val email: String)

val user = User(1, "John", "john@example.com")
println(user) // User(id=1, name=John, email=john@example.com)

// copy() - создает копию с измененными полями
val updated = user.copy(email = "new@example.com")

// Деструктуризация через componentN()
val (id, name, email) = user

Ограничения data class:

  • Должен иметь хотя бы один параметр в primary constructor
  • Все параметры должны быть val или var
  • Не может быть abstract, open, sealed, inner

Важно: Data class генерирует методы только для параметров primary constructor. Свойства, добавленные в теле класса, не участвуют в equals() и hashCode().

Value Classes (Inline Classes, Kotlin 1.5+)

// Value class - оборачивает примитив без runtime overhead
value class UserId(val value: Long)

// Компилятор может оптимизировать до примитива
val id = UserId(123L)
val userId: UserId = id  // во многих случаях избегает boxing

// Отличие от data class - нет copy(), toString() автоматический
// Используется для type-safe обертки над примитивами

Sealed class

Sealed class - ограниченная иерархия классов, все наследники известны на этапе компиляции. Отлично для состояний, результатов операций, паттерна State.

sealed class Result<T>
data class Success<T>(val data: T) : Result<T>()
data class Error<T>(val message: String) : Result<T>()
object Loading : Result<Nothing>()

// Компилятор знает все возможные типы - не нужен else
fun handleResult(result: Result<String>) = when (result) {
    is Success -> println(result.data)
    is Error -> println(result.message)
    is Loading -> println("Loading...")
}

Преимущества sealed class:

  • Exhaustive checking в when выражениях
  • Все наследники должны быть в том же файле (до Kotlin 1.5)
  • Безопасность типов на этапе компиляции

Enum class

Enum class - перечисления с дополнительными возможностями: свойства, методы, интерфейсы.

enum class Priority(val level: Int) {
    LOW(1),
    MEDIUM(5),
    HIGH(10);
    
    fun isUrgent() = level > 7
}

// Встроенные методы
Priority.valueOf("HIGH")  // Получить по имени
Priority.values()         // Все значения
Priority.HIGH.ordinal     // Порядковый номер

Object

Object - singleton pattern, ленивая инициализация, thread-safe.

// Singleton объект
object DatabaseConfig {
    val url = "jdbc:postgresql://localhost/db"
    fun connect() = println("Connecting to $url")
}

// Object expression - анонимный объект
val clickListener = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        println("Clicked!")
    }
}

// Companion object - аналог static в Java
class MyClass {
    companion object Factory {
        fun create() = MyClass()
    }
}

// Companion object с интерфейсом
class MyClass {
    companion object : Comparator<MyClass> {
        override fun compare(a: MyClass, b: MyClass): Int = 0
    }
}

@JvmStatic аннотация для совместимости с Java:

class Utils {
    companion object {
        @JvmStatic
        fun staticLikeMethod() { /* доступен как static из Java */ }
    }
}

Конструкторы и инициализация

Primary constructor

Primary constructor - главный конструктор, часть заголовка класса. Не может содержать код.

class Person(val name: String, var age: Int) {
    // val/var в конструкторе автоматически создают свойства
}

// С модификаторами видимости
class User private constructor(val id: Long) {
    // Приватный конструктор
}

// Параметры без val/var - только для инициализации
class Calculator(initialValue: Int) {
    private val value = initialValue * 2
}

Secondary constructor

Secondary constructor - дополнительные конструкторы с ключевым словом constructor. Должны вызывать primary constructor.

class Person(val name: String) {
    var age: Int = 0
    
    // Secondary constructor должен вызвать primary через this()
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
    
    // Можно создать цепочку secondary конструкторов
    constructor(name: String, age: Int, email: String) : this(name, age) {
        // дополнительная инициализация
    }
}

Init блоки

Init блоки - выполняются при создании объекта в порядке объявления, имеют доступ к параметрам primary constructor.

class User(name: String, age: Int) {
    val name: String
    val age: Int
    
    init {
        require(age >= 0) { "Age must be non-negative" }
        this.name = name.trim()
        this.age = age
        println("User created: $name")
    }
    
    init {
        // Второй init блок - выполнится после первого
        println("Second init block")
    }
}

Default параметры и Named параметры

// Default параметры
fun greet(name: String = "World", greeting: String = "Hello") {
    println("$greeting, $name!")
}

greet()                          // Hello, World!
greet("Alice")                  // Hello, Alice!
greet(greeting = "Hi")          // Hi, World!

// Named параметры упрощают чтение и позволяют пропускать параметры
data class Config(
    val host: String = "localhost",
    val port: Int = 5432,
    val user: String = "admin",
    val password: String = ""
)

val config = Config(port = 3306, password = "secret")

Наследование

Open, override, final

По умолчанию все классы и методы в Kotlin final (нельзя наследовать/переопределять).

// open - разрешает наследование
open class Animal(val name: String) {
    open fun makeSound() = "Some sound"     // open - можно переопределить
    fun sleep() = "Sleeping"                // final - нельзя переопределить
}

class Dog(name: String) : Animal(name) {
    override fun makeSound() = "Woof!"      // override обязателен
    
    final override fun toString(): String {  // final override - запрещает дальнейшее переопределение
        return "Dog: $name"
    }
}

Принципы наследования:

  • Класс должен быть open для наследования
  • Метод должен быть open для переопределения
  • override обязателен при переопределении
  • final override запрещает дальнейшее переопределение

Extension функции и свойства

Extension функции

Extension функции - добавляют функциональность к существующим классам без изменения исходного кода.

// Добавляем функцию к String
fun String.isValidEmail(): Boolean {
    return contains("@") && contains(".")
}

// Использование
val email = "user@example.com"
if (email.isValidEmail()) println("Valid email")

// Extension для generic типов
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

// Extension с nullable receiver
fun String?.isNullOrEmpty(): Boolean = this == null || this.isEmpty()

// Extension для коллекций
fun <T> List<T>.getOrDefault(index: Int, default: T): T = 
    if (index in indices) this[index] else default

Extension свойства

Extension свойства - добавляют свойства к существующим классам. Должны иметь getter, не могут иметь backing field.

val String.lastChar: Char
    get() = this[length - 1]

var StringBuilder.lastChar: Char
    get() = this[length - 1]
    set(value) { setCharAt(length - 1, value) }

// Использование
println("Hello".lastChar)  // 'o'

Модификаторы функций

Inline функции

Inline функции - компилятор встраивает код функции на место вызова, устраняя overhead вызова функции высшего порядка.

inline fun <T> measureTime(block: () -> T): T {
    val start = System.currentTimeMillis()
    val result = block()
    val end = System.currentTimeMillis()
    println("Execution time: ${end - start}ms")
    return result
}

// Компилятор заменит вызов на встроенный код
val result = measureTime {
    // некоторые вычисления
    42
}

Noinline и crossinline

Noinline - предотвращает встраивание конкретного lambda параметра.

inline fun processData(
    data: List<String>,
    noinline logger: (String) -> Unit,  // не будет встроен
    processor: (String) -> String       // будет встроен
) {
    // logger можно передать в другую функцию
    data.forEach { logger(processor(it)) }
}

Crossinline - запрещает non-local returns в lambda.

inline fun runSafely(crossinline action: () -> Unit) {
    try {
        action()  // не может содержать return из внешней функции
    } catch (e: Exception) {
        println("Error: ${e.message}")
    }
}

Infix функции

Infix функции - позволяют вызывать функции без точки и скобок, должны иметь один параметр.

infix fun Int.times(str: String) = str.repeat(this)
infix fun String.shouldBe(expected: String) = assert(this == expected)

// Использование
val result = 3 times "Hello"  // вместо 3.times("Hello")
"actual" shouldBe "expected"  // вместо "actual".shouldBe("expected")

Tailrec функции

Tailrec - оптимизирует хвостовую рекурсию, заменяя её на цикл.

tailrec fun factorial(n: Long, accumulator: Long = 1): Long {
    return if (n <= 1) accumulator
    else factorial(n - 1, n * accumulator)  // хвостовой вызов
}

// Компилятор преобразует в цикл, избегая StackOverflowError
val result = factorial(10000)

Требования для tailrec:

  • Последняя операция должна быть рекурсивный вызов
  • Нельзя использовать в try/catch блоках
  • Должна быть member или extension функция
  • Не работает с функциями, которые возвращают suspend корутины

Ключевые моменты для собеседования:

  • Data classes идеальны для immutable объектов и DTO
  • Sealed classes обеспечивают exhaustive checking и type safety
  • Extension функции - мощный инструмент для расширения API без наследования
  • Inline функции устраняют overhead lambda выражений
  • Value classes обеспечивают type-safety без runtime overhead
  • Kotlin's наследование более строгое чем в Java - explicit open/override
  • Default параметры и named параметры делают код более читаемым
  • Companion objects предоставляют функциональность static членов Java

Коллекции

Immutable vs Mutable коллекции

Концепция разделения

В Kotlin коллекции разделены на read-only (неизменяемые) и mutable (изменяемые) интерфейсы. Это обеспечивает безопасность типов и предотвращает случайные изменения.

// Read-only коллекции - интерфейсы без методов изменения
val readOnlyList: List<String> = listOf("a", "b", "c")
val readOnlySet: Set<Int> = setOf(1, 2, 3)
val readOnlyMap: Map<String, Int> = mapOf("key1" to 1, "key2" to 2)

// Mutable коллекции - расширяют read-only интерфейсы
val mutableList: MutableList<String> = mutableListOf("a", "b", "c")
val mutableSet: MutableSet<Int> = mutableSetOf(1, 2, 3)
val mutableMap: MutableMap<String, Int> = mutableMapOf("key1" to 1)

// Изменение возможно только у mutable
mutableList.add("d")
mutableSet.remove(1)
mutableMap["key3"] = 3

Принципы работы с коллекциями

// Преобразование между типами
val readOnly = mutableListOf(1, 2, 3).toList()  // возвращает read-only view
val mutable = listOf(1, 2, 3).toMutableList()   // создает новую mutable коллекцию

// Read-only НЕ значит immutable - базовая коллекция может измениться
val original = mutableListOf(1, 2, 3)
val readOnlyView: List<Int> = original  // view на original
original.add(4)  // readOnlyView теперь содержит [1, 2, 3, 4]

Важно понимать: Read-only коллекции в Kotlin - это view (представление), а не true immutable структуры данных.

Типы коллекций

List - упорядоченная коллекция с дубликатами

List - индексированная коллекция элементов, допускающая дубликаты.

// Создание
val fruits = listOf("apple", "banana", "apple")  // дубликаты разрешены
val mutableFruits = mutableListOf("orange", "grape")

// Основные операции
fruits[0]                    // "apple" - доступ по индексу
fruits.indexOf("apple")      // 0 - первое вхождение
fruits.lastIndexOf("apple")  // 2 - последнее вхождение
fruits.contains("banana")    // true

// Mutable операции
mutableFruits.add("kiwi")
mutableFruits.removeAt(0)
mutableFruits[0] = "mango"

Set - уникальные элементы без порядка

Set - коллекция уникальных элементов, дубликаты автоматически удаляются.

val uniqueNumbers = setOf(1, 2, 3, 2, 1)  // результат: {1, 2, 3}
val mutableSet = mutableSetOf<String>()

// Операции с множествами
val set1 = setOf(1, 2, 3)
val set2 = setOf(3, 4, 5)

val union = set1 union set2         // {1, 2, 3, 4, 5}
val intersection = set1 intersect set2  // {3}
val difference = set1 subtract set2     // {1, 2}

// Проверки
set1.contains(2)     // true
2 in set1           // true (infix notation)

Map - ключ-значение пары

Map - коллекция пар ключ-значение, каждый ключ уникален.

val ages = mapOf("John" to 25, "Jane" to 30)
val mutableAges = mutableMapOf<String, Int>()

// Доступ к значениям
ages["John"]        // 25 (nullable)
ages.getValue("John")  // 25 (бросает исключение если нет ключа)
ages.getOrDefault("Bob", 0)  // 0

// Операции
ages.keys           // {\"John\", \"Jane\"}
ages.values         // {25, 30}
ages.entries        // {Entry(John=25), Entry(Jane=30)}

// Mutable операции
mutableAges["Alice"] = 28
mutableAges.remove("John")
mutableAges.putAll(mapOf("Bob" to 35, "Carol" to 32))

// Трансформация Map
val filterKeys = ages.filterKeys { it.startsWith("J") }
val filterValues = ages.filterValues { it > 20 }
val mapKeys = ages.mapKeys { (k, v) -> k.uppercase() }
val mapValues = ages.mapValues { (k, v) -> v + 1 }

Специализированные операции группировки

GroupBy - группировка по ключу

GroupBy - группирует элементы коллекции по результату функции, возвращает Map<Key, List<Element>>.

data class Person(val name: String, val age: Int, val city: String)

val people = listOf(
    Person("John", 25, "NYC"),
    Person("Jane", 30, "NYC"), 
    Person("Bob", 25, "LA")
)

// Группировка по возрасту
val byAge = people.groupBy { it.age }
// {25=[Person(John,25,NYC), Person(Bob,25,LA)], 30=[Person(Jane,30,NYC)]}

// Группировка с трансформацией значений
val namesByCity = people.groupBy({ it.city }, { it.name })
// {\"NYC\"=[\"John\", \"Jane\"], \"LA\"=[\"Bob\"]}

// Подсчет элементов в группах
val countByCity = people.groupingBy { it.city }.eachCount()
// {\"NYC\"=2, \"LA\"=1}

AssociateBy - создание Map с уникальными ключами

AssociateBy - создает Map, где каждый элемент становится значением, а ключ вычисляется функцией. При дублировании ключей остается последний элемент.

val people = listOf(
    Person("John", 25, "NYC"),
    Person("Jane", 30, "NYC"),
    Person("Bob", 25, "LA")
)

// Создание Map где ключ - имя, значение - объект Person
val peopleByName = people.associateBy { it.name }
// {\"John\"=Person(John,25,NYC), \"Jane\"=Person(Jane,30,NYC), \"Bob\"=Person(Bob,25,LA)}

// С трансформацией значения
val agesByName = people.associateBy({ it.name }, { it.age })
// {\"John\"=25, \"Jane\"=30, \"Bob\"=25}

// Associate - создание Map из пар
val cityToUppercase = people.associate { it.name to it.city.uppercase() }
// {\"John\"=\"NYC\", \"Jane\"=\"NYC\", \"Bob\"=\"LA\"}

Отличие между groupBy и associateBy:

  • groupBy: когда ключи могут повторяться - результат Map<Key, List>
  • associateBy: для 1-to-1 соответствия - результат Map<Key, Value>

Partition - разделение на две группы

Partition - разделяет коллекцию на две части по условию, возвращает Pair<List<T>, List<T>>.

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// Разделение на четные и нечетные
val (evens, odds) = numbers.partition { it % 2 == 0 }
// evens = [2, 4, 6, 8, 10], odds = [1, 3, 5, 7, 9]

val people = listOf(
    Person("John", 17, "NYC"),
    Person("Jane", 25, "NYC"),
    Person("Bob", 30, "LA")
)

// Разделение по возрасту
val (adults, minors) = people.partition { it.age >= 18 }
// adults = [Person(Jane,25,NYC), Person(Bob,30,LA)]
// minors = [Person(John,17,NYC)]

Iterable vs Sequence: ленивые вычисления

Iterable - жадные вычисления

Iterable - обычные коллекции выполняют операции немедленно (eager evaluation). Каждая операция создает промежуточную коллекцию.

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val result = numbers
    .map { println("Mapping $it"); it * it }      // выполнится для ВСЕХ элементов
    .filter { println("Filtering $it"); it > 10 } // создастся промежуточный список
    .take(3)                                      // возьмет первые 3
// Создается 3 промежуточные коллекции

Sequence - ленивые вычисления

Sequence - выполняет операции лениво (lazy evaluation). Промежуточные операции не выполняются до терминальной операции.

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

val result = numbers.asSequence()
    .map { println("Mapping $it"); it * it }      // НЕ выполнится сразу
    .filter { println("Filtering $it"); it > 10 } // НЕ выполнится сразу
    .take(3)                                      // НЕ выполнится сразу
    .toList()                                     // ЗДЕСЬ выполнятся все операции

// Обработает только необходимые элементы: 1,2,3,4 (до получения 3 результатов)

Создание Sequence

// Из коллекции
val sequence1 = listOf(1, 2, 3).asSequence()

// Генерирование
val sequence2 = generateSequence(1) { it + 1 }  // бесконечная последовательность
val sequence3 = sequenceOf(1, 2, 3, 4, 5)

// Sequence builder
val sequence4 = sequence {
    yield(1)
    yield(2)
    yieldAll(listOf(3, 4, 5))
}

// Бесконечные последовательности - используйте для начальной генерации
val fibonacci = generateSequence(1 to 1) { (a, b) -> b to (a + b) }
    .map { it.first }
    .take(10)
    .toList()  // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Когда использовать Sequence

// Используйте Sequence когда:

// 1. Большие коллекции с цепочками операций
val largeList = (1..1_000_000).toList()
val result = largeList.asSequence()
    .map { it * 2 }
    .filter { it > 100 }
    .take(10)
    .toList()

// 2. Потенциально бесконечные данные
val fibonacci = generateSequence(1 to 1) { (a, b) -> b to (a + b) }
    .map { it.first }
    .take(10)
    .toList()

// 3. Early termination (досрочное завершение)
val firstEven = (1..1000).asSequence()
    .map { heavyComputation(it) }  // выполнится только до первого четного
    .first { it % 2 == 0 }

Производительность: Iterable vs Sequence

val size = 1_000_000
val data = (1..size).toList()

// Iterable - создает промежуточные коллекции
val iterableResult = data
    .map { it * 2 }        // создает список из 1M элементов
    .filter { it > 100 }   // создает новый список
    .take(100)             // создает список из 100 элементов

// Sequence - обрабатывает элементы по одному
val sequenceResult = data.asSequence()
    .map { it * 2 }        // ленивая операция
    .filter { it > 100 }   // ленивая операция  
    .take(100)             // ленивая операция
    .toList()              // здесь выполняется вся цепочка для ~50 элементов

Операции Sequence, которых нет в Iterable

// drop и take работают эффективнее
val dropped = sequence.drop(100).take(10)

// chunked - разбивает на подпоследовательности
val chunks = listOf(1, 2, 3, 4, 5, 6, 7, 8).asSequence().chunked(3)
// [[1, 2, 3], [4, 5, 6], [7, 8]]

// windowed - скользящее окно
val windows = listOf(1, 2, 3, 4, 5).asSequence().windowed(2)
// [[1, 2], [2, 3], [3, 4], [4, 5]]

// zipWithNext - соседние элементы
val pairs = listOf(1, 2, 3, 4).asSequence().zipWithNext()
// [(1, 2), (2, 3), (3, 4)]

Ключевые правила выбора:

  • Iterable: маленькие коллекции, простые операции, нужен весь результат
  • Sequence: большие коллекции, сложные цепочки операций, early termination, потенциально бесконечные данные

Важные моменты для собеседования:

  • Read-only коллекции в Kotlin - это view, не immutable структуры
  • GroupBy создает Map<Key, List>, associateBy создает Map<Key, Value>
  • Sequence использует lazy evaluation и обрабатывает элементы по одному
  • Выбор между Iterable и Sequence зависит от размера данных и паттерна использования
  • Partition всегда возвращает именно две коллекции в Pair
  • Для трансформаций Map используйте filterKeys, filterValues, mapKeys, mapValues

Функциональное программирование

Лямбды и анонимные функции

Лямбды (Lambda expressions)

Лямбда - анонимная функция, которая может быть присвоена переменной или передана как параметр. Синтаксис: { параметры -> тело функции }.

// Простая лямбда
val sum: (Int, Int) -> Int = { a, b -> a + b }

// Лямбда с одним параметром - можно использовать 'it'
val double: (Int) -> Int = { it * 2 }

// Лямбда без параметров
val greet: () -> String = { "Hello!" }

// Использование в функциях высшего порядка
listOf(1, 2, 3).map { it * 2 }  // [2, 4, 6]

Анонимные функции

Анонимные функции - альтернатива лямбдам с явным указанием типа возвращаемого значения и поддержкой return.

// Анонимная функция
val multiply = fun(a: Int, b: Int): Int { return a * b }

// С выводом типа
val add = fun(a: Int, b: Int) = a + b

// В качестве параметра
listOf(1, 2, 3).filter(fun(x): Boolean { return x > 1 })

Ключевые различия:

  • Лямбды: краткий синтаксис, return выходит из внешней функции
  • Анонимные функции: явный тип возврата, return выходит из самой функции

Функции обработки коллекций

Map - трансформация элементов

Map - преобразует каждый элемент коллекции, возвращая новую коллекцию того же размера.

val numbers = listOf(1, 2, 3, 4)
val doubled = numbers.map { it * 2 }        // [2, 4, 6, 8]
val strings = numbers.map { "Number: $it" } // ["Number: 1", "Number: 2", ...]

// mapNotNull - исключает null значения
val mixed = listOf("1", "2", "abc", "4")
val parsed = mixed.mapNotNull { it.toIntOrNull() }  // [1, 2, 4]

// mapIndexed - с индексом элемента
val indexed = numbers.mapIndexed { index, value -> "$index: $value" }
// ["0: 1", "1: 2", "2: 3", "3: 4"]

Filter - фильтрация элементов

Filter - отбирает элементы по условию, возвращая новую коллекцию меньшего или равного размера.

val numbers = listOf(1, 2, 3, 4, 5, 6)
val evens = numbers.filter { it % 2 == 0 }     // [2, 4, 6]
val odds = numbers.filterNot { it % 2 == 0 }   // [1, 3, 5]

// filterIsInstance - фильтрует по типу
val mixed = listOf(1, "hello", 2, "world")
val strings = mixed.filterIsInstance<String>()  // ["hello", "world"]

// Комбинирование фильтров
val result = numbers
    .filter { it > 2 }
    .filter { it < 6 }  // [3, 4, 5]

Fold и Reduce - агрегация

Fold - свертывает коллекцию к одному значению, используя начальное значение. Reduce - то же самое, но без начального значения, использует первый элемент.

val numbers = listOf(1, 2, 3, 4, 5)

// fold - с начальным значением
val sum = numbers.fold(0) { acc, element -> acc + element }  // 15
val product = numbers.fold(1) { acc, element -> acc * element }  // 120

// reduce - без начального значения
val sum2 = numbers.reduce { acc, element -> acc + element }  // 15
val max = numbers.reduce { acc, element -> if (acc > element) acc else element }  // 5

// foldRight/reduceRight - справа налево
val rightSum = numbers.foldRight(0) { element, acc -> element + acc }

// scan - возвращает все промежуточные результаты
val scanned = numbers.scan(0) { acc, x -> acc + x }
// [0, 1, 3, 6, 10, 15]

Отличия fold vs reduce:

  • fold безопасен для пустых коллекций (возвращает начальное значение)
  • reduce бросает исключение на пустой коллекции
  • fold может менять тип результата
  • scan может быть полезен для промежуточных значений

FlatMap - выравнивание коллекций

FlatMap - преобразует каждый элемент в коллекцию, затем объединяет все в одну плоскую коллекцию.

val words = listOf("hello", "world")
val chars = words.flatMap { it.toList() }  // ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

val numbers = listOf(1, 2, 3)
val pairs = numbers.flatMap { n -> listOf(n, n * 2) }  // [1, 2, 2, 4, 3, 6]

// flatten - просто объединяет коллекции коллекций
val nested = listOf(listOf(1, 2), listOf(3, 4))
val flat = nested.flatten()  // [1, 2, 3, 4]

Дополнительные операции коллекций

Any, All, None, Find

val numbers = listOf(1, 2, 3, 4, 5)

// any - есть ли хотя бы один элемент, удовлетворяющий условию
val hasEven = numbers.any { it % 2 == 0 }  // true

// all - все ли элементы удовлетворяют условию
val allPositive = numbers.all { it > 0 }  // true

// none - ни один элемент не удовлетворяет условию
val noNegative = numbers.none { it < 0 }  // true

// find/firstOrNull - найти первый элемент
val firstEven = numbers.find { it % 2 == 0 }  // 2
val notFound = numbers.find { it > 100 }  // null

// findLast/lastOrNull
val lastEven = numbers.findLast { it % 2 == 0 }  // 4

// count - количество элементов
val evenCount = numbers.count { it % 2 == 0 }  // 3

// sumOf - сумма преобразованных элементов
val sum = numbers.sumOf { it * 2 }  // 30

Deсtructuring в lambda

val pairs = listOf(1 to "a", 2 to "b", 3 to "c")

// Деструктуризация в lambda
pairs.forEach { (number, letter) ->
    println("$number: $letter")
}

// С Map entries
val map = mapOf("a" to 1, "b" to 2)
map.forEach { (key, value) ->
    println("$key: $value")
}

// С индексом и деструктуризацией
pairs.forEachIndexed { index, (number, letter) ->
    println("$index: $number, $letter")
}

Scope-функции: детальное сравнение

With - контекстное выполнение

With - выполняет блок кода в контексте объекта, не является extension функцией.

val person = Person("John", 25)
val result = with(person) {
    println("Name: $name")  // this.name
    println("Age: $age")    // this.age
    "Person processed"      // возвращаемое значение
}

Apply - конфигурация объекта

Apply - вызывается на объекте, выполняет блок кода и возвращает сам объект. Идеален для Builder pattern.

val person = Person().apply {
    name = "John"    // this.name = "John"
    age = 25         // this.age = 25
    email = "john@example.com"
}
// person содержит настроенный объект

Run - комбинация with и let

Run - выполняет блок кода в контексте объекта и возвращает результат блока.

val result = person.run {
    validateAge()        // методы объекта
    "${name} is ${age}"  // возвращаемое значение
}

// Также как standalone функция
val result2 = run {
    val a = 10
    val b = 20
    a + b  // 30
}

Also - дополнительные действия

Also - выполняет блок кода с объектом и возвращает сам объект. Используется для side effects.

val numbers = mutableListOf(1, 2, 3)
    .also { println("Original: $it") }  // it = [1, 2, 3]
    .also { it.add(4) }                 // добавляем элемент
    .also { println("Modified: $it") }  // it = [1, 2, 3, 4]

Let - обработка nullable и трансформация

Let - выполняет блок кода с объектом как параметром и возвращает результат блока.

val name: String? = "John"
val result = name?.let { 
    println("Name is $it")  // it = "John"
    it.uppercase()          // возвращает "JOHN"
}

// Часто используется для цепочки вызовов
val result2 = listOf(1, 2, 3)
    .let { it.filter { n -> n > 1 } }  // [2, 3]
    .let { it.map { n -> n * 2 } }     // [4, 6]

DSL-подходы (Domain Specific Language)

DSL - предметно-ориентированный язык, создающий читаемый API для конкретной области.

Простой DSL для HTML

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

class HTML {
    fun head(init: Head.() -> Unit) = Head().apply(init)
    fun body(init: Body.() -> Unit) = Body().apply(init)
}

class Head {
    fun title(text: String) = println("<title>$text</title>")
}

class Body {
    fun h1(text: String) = println("<h1>$text</h1>")
    fun p(text: String) = println("<p>$text</p>")
}

// Использование DSL
html {
    head {
        title("My Page")
    }
    body {
        h1("Welcome")
        p("This is a paragraph")
    }
}

DSL для конфигурации

class DatabaseConfig {
    var host: String = "localhost"
    var port: Int = 5432
    var username: String = ""
    var password: String = ""
}

fun database(init: DatabaseConfig.() -> Unit): DatabaseConfig {
    return DatabaseConfig().apply(init)
}

// Использование
val config = database {
    host = "production.db.com"
    port = 3306
    username = "admin"
    password = "secret"
}

Ключевые техники DSL

  • Function literals with receiver - T.() -> Unit
  • Infix functions - для читаемого синтаксиса
  • Operator overloading - для специальных операторов
  • Extension functions - для расширения API
// Инфиксная функция для DSL
infix fun String.shouldContain(substring: String) {
    assert(this.contains(substring)) { "$this should contain $substring" }
}

// Использование
"Hello World" shouldContain "World"

// Operator overloading для DSL
class SqlQuery {
    private val conditions = mutableListOf<String>()
    
    operator fun String.unaryPlus() {
        conditions.add(this)
    }
    
    fun build() = conditions.joinToString(" AND ")
}

fun query(init: SqlQuery.() -> Unit) = SqlQuery().apply(init)

// Использование
val sql = query {
    +"age > 18"
    +"status = 'active'"
}.build()  // "age > 18 AND status = 'active'"

Важные моменты для собеседования:

  • Функциональный стиль в Kotlin улучшает читаемость и сокращает boilerplate код
  • Scope-функции решают разные задачи - важно выбирать правильную для конкретной ситуации
  • DSL позволяет создавать читаемые API, особенно полезно для конфигурации и builder'ов
  • Nullable chain устраняет необходимость в множественных null-проверках
  • Комбинирование функций высшего порядка создает мощные data processing pipelines
  • Деструктуризация в lambda делает код более выразительным
  • any, all, none предпочтительнее логических цепочек при работе с условиями

Generics

Основы обобщений

Обобщенные классы

Generic класс - класс, параметризованный одним или несколькими типами. Позволяет создавать type-safe коллекции и контейнеры.

// Простой generic класс
class Box<T>(val value: T) {
    fun get(): T = value
}

val stringBox = Box<String>("Hello")  // явное указание типа
val intBox = Box(42)                  // автовывод типа

// Множественные параметры типа
class Pair<A, B>(val first: A, val second: B)
val pair = Pair<String, Int>("key", 42)

Обобщенные функции

Generic функции - функции с параметрами типа, которые могут работать с разными типами, сохраняя type safety.

// Generic функция с одним параметром типа
fun <T> singletonList(item: T): List<T> {
    return listOf(item)
}

val strings = singletonList("hello")  // List<String>
val numbers = singletonList(42)       // List<Int>

// Множественные параметры типа
fun <K, V> mapOf(key: K, value: V): Map<K, V> {
    return kotlin.collections.mapOf(key to value)
}

// Generic extension функция
fun <T> T.toSingletonList(): List<T> = listOf(this)

Вариантность: in, out, *

Out (коваринтность) - производитель

Out означает, что тип является производителем (producer) значений типа T. Тип может только возвращать T, но не принимать его как параметр.

// Коваринтный интерфейс - только возвращает T
interface Producer<out T> {
    fun produce(): T                    // OK - возвращаем T
    // fun consume(item: T)             // ОШИБКА - нельзя принимать T
}

class StringProducer : Producer<String> {
    override fun produce(): String = "Hello"
}

// Коваринтность позволяет присваивать более специфический тип более общему
val stringProducer: Producer<String> = StringProducer()
val anyProducer: Producer<Any> = stringProducer  // OK: String <: Any

// Практический пример - List<out T> в Kotlin
val strings: List<String> = listOf("a", "b")
val anyList: List<Any> = strings  // OK, потому что List<out T>

In (контрвариантность) - потребитель

In означает, что тип является потребителем (consumer) значений типа T. Тип может только принимать T как параметр, но не возвращать его.

// Контрвариантный интерфейс - только принимает T
interface Consumer<in T> {
    fun consume(item: T)               // OK - принимаем T
    // fun produce(): T                // ОШИБКА - нельзя возвращать T
}

class AnyConsumer : Consumer<Any> {
    override fun consume(item: Any) {
        println("Consuming: $item")
    }
}

// Контрвариантность позволяет присваивать более общий тип более специфическому
val anyConsumer: Consumer<Any> = AnyConsumer()
val stringConsumer: Consumer<String> = anyConsumer  // OK: Any >: String

Invariant - инвариантность

// Инвариантный тип - ни коварианта, ни контрвариантна
interface Mutable<T> {
    fun get(): T
    fun set(item: T)
}

val stringMutable: Mutable<String> = /* ... */
// val anyMutable: Mutable<Any> = stringMutable  // ОШИБКА - неинвариантно

Star projection (*) - неизвестный тип

Star projection используется когда тип неизвестен или не важен. Эквивалентен out Any? для коваринтных типов и in Nothing для контрвариантных.

// Star projection для коваринтных типов
val unknownList: List<*> = listOf("a", "b", "c")  // эквивалент List<out Any?>
val item: Any? = unknownList.first()              // можем получить Any?
// unknownList.add("x")                           // ОШИБКА - нельзя добавлять

// Star projection для контрвариантных типов  
val unknownComparator: Comparator<*> = Comparator<String> { a, b -> a.compareTo(b) }
// unknownComparator.compare("a", "b")            // ОШИБКА - нельзя вызывать с аргументами

// Полезно для проверки типов без знания параметров
fun processList(list: List<*>) {
    println("List size: ${list.size}")
    // Можем работать с методами, не зависящими от типа элементов
}

Правило PECS (Producer Extends, Consumer Super)

// Producer - используйте out (extends в Java)
fun fillList(source: List<out Number>, destination: MutableList<Number>) {
    for (item in source) {
        destination.add(item)  // source может содержать Int, Double, etc.
    }
}

// Consumer - используйте in (super в Java)
fun copyTo(source: List<Number>, destination: MutableList<in Number>) {
    for (item in source) {
        destination.add(item)  // destination может принимать Number и его супертипы
    }
}

Reified - сохранение информации о типе

Проблема type erasure

В JVM информация о generic типах стирается во время компиляции. Reified в inline функциях позволяет сохранить эту информацию.

// Обычная функция - type erasure
fun <T> isOfType(value: Any): Boolean {
    // return value is T  // ОШИБКА - информация о T стерта
    return false
}

// Inline функция с reified - тип сохраняется
inline fun <reified T> isOfType(value: Any): Boolean {
    return value is T  // OK - информация о T доступна
}

// Использование
val result1 = isOfType<String>("hello")  // true
val result2 = isOfType<Int>("hello")     // false

Практические применения reified

// Создание объектов по типу
inline fun <reified T> createInstance(): T {
    return T::class.java.getDeclaredConstructor().newInstance()
}

// Фильтрация по типу
inline fun <reified T> List<*>.filterIsInstance(): List<T> {
    return this.filter { it is T }.map { it as T }
}

val mixed = listOf(1, "hello", 2, "world", 3.14)
val strings = mixed.filterIsInstance<String>()  // ["hello", "world"]

// JSON десериализация (популярный пример)
inline fun <reified T> fromJson(json: String): T {
    return Gson().fromJson(json, T::class.java)
}

val user = fromJson<User>("{\"name\":\"John\"}")

Ограничения reified

// Работает только с inline функциями
inline fun <reified T> validExample() { /* OK */ }

// НЕ работает с обычными функциями
// fun <reified T> invalidExample() { /* ОШИБКА */ }

// НЕ работает в свойствах классов
class Container<T> {
    // val type = T::class  // ОШИБКА - type erasure
}

// Не работает с suspend функциями
// inline suspend fun <reified T> suspendExample() { /* ОШИБКА */ }

Ограничения типов (Type bounds)

Простые ограничения

Type bounds ограничивают возможные типы для generic параметров, обеспечивая доступ к методам супертипа.

// Ограничение одним супертипом
fun <T : Number> sumValues(values: List<T>): Double {
    return values.sumOf { it.toDouble() }  // toDouble() доступен через Number
}

val intSum = sumValues(listOf(1, 2, 3))        // OK - Int : Number
val doubleSum = sumValues(listOf(1.0, 2.0))    // OK - Double : Number
// val stringSum = sumValues(listOf("a", "b"))  // ОШИБКА - String !: Number

// Ограничение nullable типом
fun <T : Any> nonNullOnly(value: T): T {
    return value  // T гарантированно non-null
}

Множественные ограничения (where)

Where clause позволяет задать несколько ограничений для одного типа.

// Тип должен реализовывать и Comparable, и Serializable
fun <T> sortAndSerialize(items: List<T>): String 
    where T : Comparable<T>, 
          T : java.io.Serializable {
    val sorted = items.sorted()  // доступно через Comparable
    return sorted.toString()     // сериализация доступна
}

// Более сложный пример с множественными типами
fun <T, U> processData(data: T, processor: U): String
    where T : Collection<String>,
          T : Iterable<String>,
          U : Function<String, String> {
    return data.map { processor.apply(it) }.joinToString()
}

Ограничения в классах

// Ограничения в объявлении класса
class SortedList<T : Comparable<T>> {
    private val items = mutableListOf<T>()
    
    fun add(item: T) {
        items.add(item)
        items.sort()  // sort() доступен через Comparable
    }
}

// Множественные ограничения в классах
class DataProcessor<T> where T : Readable, T : Closeable {
    fun process(resource: T) {
        try {
            val data = resource.read()  // доступно через Readable
            // обработка данных
        } finally {
            resource.close()  // доступно через Closeable
        }
    }
}

Продвинутые паттерны

Declaration-site vs Use-site variance

// Declaration-site variance (в объявлении)
class Box<out T>(private val data: T) {
    fun get(): T = data
}

val box: Box<String> = Box("hello")
val anyBox: Box<Any> = box  // OK - covariant

// Use-site variance (при использовании)
fun <T> process(item: T, consumer: (T) -> Unit) { /* ... */ }
fun printAll(items: List<out Any>) {  // out - только для чтения
    items.forEach { println(it) }
}

fun addAll(items: MutableList<in String>) {  // in - только для записи
    items.add("hello")
}

Self-type (рекурсивные ограничения)

// Паттерн для builder'ов и fluent API
abstract class SelfReturning<SELF : SelfReturning<SELF>> {
    abstract fun self(): SELF
    
    fun commonMethod(): SELF {
        // общая логика
        return self()
    }
}

class ConcreteBuilder : SelfReturning<ConcreteBuilder>() {
    override fun self(): ConcreteBuilder = this
    
    fun specificMethod(): ConcreteBuilder {
        // специфичная логика
        return this
    }
}

// Использование - все методы возвращают правильный тип
val builder = ConcreteBuilder()
    .commonMethod()    // возвращает ConcreteBuilder
    .specificMethod()  // возвращает ConcreteBuilder

Nothing тип

// Nothing - тип без значений, используется для функций которые не возвращаются
fun fail(message: String): Nothing {
    throw Exception(message)
}

// Полезно в контрвариантных позициях
sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}

// Бесконечные последовательности
fun infinite(): List<Nothing> = listOf()  // empty list совместим с любым типом

Ключевые моменты для собеседования:

  • Out/In решают проблему вариантности - out для producers, in для consumers
  • Star projection используется когда конкретный тип неважен
  • Reified работает только с inline функциями и решает проблему type erasure
  • Where позволяет задавать множественные ограничения типов
  • Declaration-site variance более безопасна и явна чем use-site
  • Kotlin's generics более выразительны чем в Java благодаря declaration-site variance
  • Nothing тип полезен для функций, которые не возвращаются
  • Правило PECS помогает правильно выбирать in/out вариантность

Kotlin Scope Functions

Что такое Scope Functions?

Scope Functions (функции области видимости) — это функции высшего порядка в Kotlin, которые позволяют выполнить блок кода в контексте определённого объекта. Основная цель — сделать код более читаемым и лаконичным, избежать повторения имён переменных.

Ключевые понятия:

  • Контекстный объект — объект, для которого вызывается scope function
  • Lambda receiver — способ доступа к контекстному объекту внутри блока
  • Возвращаемое значение — что возвращает функция после выполнения

Основные scope functions

1. let

Назначение: Выполнение операций с объектом и возврат результата lambda Доступ к объекту: через параметр it Возвращает: результат lambda

// Проверка на null и преобразование
val result = name?.let {
    println("Имя: $it")
    it.uppercase() // возвращается результат
}

// Цепочка вызовов
listOf(1, 2, 3)
    .let { it.filter { num -> num > 1 } }
    .let { println("Отфильтрованный список: $it") }

Когда использовать:

  • Проверка на null с безопасным вызовом ?.let
  • Преобразование объекта и возврат результата
  • Ограничение области видимости переменной

2. run

Назначение: Выполнение блока кода как extension function Доступ к объекту: через this (можно опускать) Возвращает: результат lambda

// Инициализация объекта
val person = Person().run {
    name = "Иван"
    age = 30
    this // можно опустить, вернётся автоматически
}

// Выполнение операций и возврат результата
val result = service.run {
    connect()
    fetchData()
    processData() // возвращается результат этого метода
}

Когда использовать:

  • Инициализация объекта с несколькими свойствами
  • Выполнение логики в контексте объекта с возвратом результата
  • Замена блоков with, когда нужен возврат значения

3. with

Назначение: Выполнение операций с объектом (не extension function) Доступ к объекту: через this Возвращает: результат lambda

// Работа с существующим объектом
val person = Person()
val result = with(person) {
    name = "Мария"
    age = 25
    getFullInfo() // возвращается результат
}

// Группировка операций
with(canvas) {
    drawLine(0, 0, 100, 100)
    drawCircle(50, 50, 25)
    save()
}

Когда использовать:

  • Группировка операций над объектом
  • Когда объект передаётся как параметр, а не как receiver
  • Читаемость кода при множественных операциях

4. apply

Назначение: Конфигурация объекта Доступ к объекту: через this Возвращает: сам объект (контекстный объект)

// Создание и настройка объекта
val person = Person().apply {
    name = "Пётр"
    age = 35
    email = "petr@example.com"
}

// Настройка View
val textView = TextView(context).apply {
    text = "Заголовок"
    textSize = 18f
    setTextColor(Color.BLACK)
}

// В цепочке операций
val config = DatabaseConfig().apply {
    host = "localhost"
    port = 5432
}.also { saveToCache(it) }

Когда использовать:

  • Инициализация объекта с установкой свойств
  • Builder pattern в Kotlin
  • Конфигурация объектов (особенно UI компонентов)

5. also

Назначение: Дополнительные действия с объектом (side effects) Доступ к объекту: через параметр it Возвращает: сам объект (контекстный объект)

// Логирование и дополнительные действия
val numbers = mutableListOf(1, 2, 3).also {
    println("Создан список: $it")
    log.info("Размер списка: ${it.size}")
}

// Цепочка с side effects
val result = processData()
    .also { println("Данные обработаны: $it") }
    .also { saveToCache(it) }
    .also { notifyObservers(it) }
    .transform()

Когда использовать:

  • Логирование промежуточных результатов
  • Выполнение side effects без изменения основного потока
  • Дебаг и мониторинг в цепочках вызовов

Сравнительная таблица

Функция Доступ к объекту Возвращает Основное назначение
let it Результат lambda Преобразование, null-safety
run this Результат lambda Выполнение логики в контексте
with this Результат lambda Группировка операций
apply this Сам объект Конфигурация объекта
also it Сам объект Side effects, логирование

Практические примеры для собеседования

Null Safety

// Плохо
if (user != null) {
    println(user.name)
    saveUser(user)
}

// Хорошо
user?.let {
    println(it.name)
    saveUser(it)
}

Builder Pattern

// Создание конфигурации
val config = DatabaseConfig().apply {
    host = "localhost"
    port = 5432
    database = "myapp"
    username = "admin"
}

Цепочка обработки

val result = inputData
    .let { validateInput(it) }
    .also { println("Валидация пройдена") }
    .let { processData(it) }
    .also { logResult(it) }
    .let { formatOutput(it) }

Real-world примеры

Spring Boot контроллер

@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): UserResponse = getUserFromDb(id)
    .let { user -> UserResponse.from(user) }
    .also { logger.info("Returning user: ${it.id}") }

Ktor маршруты

routing {
    get("/users") {
        database.getAllUsers()
            .also { logger.debug("Found ${it.size} users") }
            .let { call.respond(it) }
    }
}

Работа с ресурсами

File("config.properties").bufferedReader().use { reader ->
    Properties().apply {
        load(reader)
    }.also { props ->
        logger.info("Loaded ${props.size} properties")
    }
}

Ключевые отличия для интервью

let vs run

  • let: доступ через it, удобно для null-safety
  • run: доступ через this, как extension function

apply vs also

  • apply: для конфигурации (this), возвращает объект
  • also: для side effects (it), возвращает объект

run vs with

  • run: extension function, может быть вызвана с ?.
  • with: обычная функция, принимает объект как параметр

Когда использовать it vs this

  • it: когда объект рассматривается как параметр (let, also)
  • this: когда работаем в контексте объекта (run, apply, with)

Частые ошибки

  1. Путаница с возвращаемыми значениями

    // Ошибка: apply возвращает объект, а не результат lambda
    val length = "Hello".apply { this.length } // вернётся "Hello"
    
    // Правильно
    val length = "Hello".let { it.length } // вернётся 5
    
  2. Неправильный выбор функции

    // Избыточно
    person.let { it.name = "Иван" }
    
    // Правильно
    person.apply { name = "Иван" }
    
  3. Нарушение принципа single responsibility

    // Плохо: смешиваем конфигурацию и side effects
    person.apply {
        name = "Иван"
        println("Создан пользователь") // side effect
    }
    
    // Хорошо
    person.apply { name = "Иван" }
          .also { println("Создан пользователь") }
    

Производительность и инлайнинг

Все scope функции объявлены как inline, что означает:

  • Код lambda встраивается на место вызова
  • Нет overhead функции высшего порядка
  • Возможна оптимизация компилятором
// Для receiver функций компилятор генерирует extension
inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

// Использование
val result = object.apply {
    // компилятор заменит это на прямой вызов methods на object
    property = value
    method()
}

Ключевые моменты для собеседования:

  • Scope functions делают код более читаемым и выразительным
  • Каждая функция решает конкретную задачу - выбирайте правильную
  • Они встраиваются компилятором - нет производительности overhead
  • Полезны для null-safety, конфигурации и логирования
  • Комбинирование scope functions создает сильные data pipelines
  • Real-world применение в Spring Boot, Ktor, и других фреймворках

Kotlin Sealed Classes and Interfaces

Что такое Sealed Classes?

Sealed classes (запечатанные классы) — это ограниченная иерархия классов, где все наследники должны быть объявлены в том же файле. Это обеспечивает закрытое множество типов — компилятор знает все возможные подтипы на этапе компиляции.

Ключевые особенности:

  • Закрытая иерархия — нельзя добавить новые наследники извне
  • Exhaustive when — компилятор требует обработки всех вариантов
  • Type safety — гарантия отсутствия неожиданных типов во время выполнения

Sealed Classes

Базовый синтаксис

sealed class Result<T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error<T>(val message: String, val code: Int) : Result<T>()
    data class Loading<T> : Result<T>()
}

Объяснение:

  • sealed class создаёт закрытую иерархию
  • Все наследники должны быть в том же файле (или пакете в Kotlin 1.5+)
  • Поддерживает generics для типобезопасности
  • data class автоматически генерирует equals, hashCode, toString

Практическое применение

// Обработка HTTP ответов
fun handleResponse(result: Result<User>) = when (result) {
    is Result.Success -> showUser(result.data)
    is Result.Error -> showError(result.message)
    is Result.Loading -> showSpinner()
    // else не нужен — компилятор знает все варианты
}

// Состояния UI
sealed class UiState {
    object Idle : UiState()
    object Loading : UiState()
    data class Content(val items: List<String>) : UiState()
    data class Error(val throwable: Throwable) : UiState()
}

Когда использовать sealed classes:

  • Конечное множество состояний (UI states, результаты операций)
  • Type-safe enum с дополнительными данными
  • Pattern matching с гарантией exhaustiveness

Sealed Interfaces (Kotlin 1.5+)

Отличия от sealed classes

sealed interface ApiResponse
data class SuccessResponse(val data: String) : ApiResponse
data class ErrorResponse(val error: String) : ApiResponse

// Класс может реализовать несколько sealed interfaces
sealed interface Loadable
sealed interface Cacheable

data class DataState(val content: String) : ApiResponse, Loadable, Cacheable

Преимущества sealed interfaces:

  • Множественная реализация — класс может реализовать несколько интерфейсов
  • Композиция вместо наследования — более гибкая архитектура
  • Совместимость с Java — интерфейсы лучше интегрируются

Практические примеры

// Архитектурные слои
sealed interface DomainEvent
data class UserLoggedIn(val userId: String) : DomainEvent
data class OrderCreated(val orderId: String) : DomainEvent

// Обработка событий
fun handleEvent(event: DomainEvent) = when (event) {
    is UserLoggedIn -> updateUserSession(event.userId)
    is OrderCreated -> sendNotification(event.orderId)
}

// Spring Boot Application Event
sealed interface ApplicationEvent : DomainEvent
data class UserRegisteredEvent(val email: String) : ApplicationEvent
data class PaymentProcessedEvent(val amount: BigDecimal) : ApplicationEvent

Сравнение с альтернативами

Sealed vs Enum

// Enum — только константы
enum class Status { SUCCESS, ERROR, LOADING }

// Sealed — с дополнительными данными
sealed class RequestState {
    object Loading : RequestState()
    data class Success(val data: Any) : RequestState()
    data class Failure(val exception: Throwable) : RequestState()
}

Когда использовать:

  • Enum: простые константы без данных
  • Sealed: состояния с ассоциированными данными

Sealed vs Abstract

// Abstract — открытая иерархия
abstract class Shape {
    abstract fun area(): Double
}
// Кто угодно может наследоваться

// Sealed — закрытая иерархия
sealed class ValidationResult {
    object Valid : ValidationResult()
    data class Invalid(val errors: List<String>) : ValidationResult()
}
// Только объявленные наследники

Ключевая разница:

  • Abstract: открытая иерархия, можно добавлять наследников
  • Sealed: закрытая иерархия, exhaustive pattern matching

Паттерны использования

1. Result/Either Pattern

sealed class Either<out L, out R> {
    data class Left<out L>(val value: L) : Either<L, Nothing>()
    data class Right<out R>(val value: R) : Either<Nothing, R>()
}

// Использование в функциональном стиле
fun divide(a: Int, b: Int): Either<String, Int> = 
    if (b == 0) Either.Left("Division by zero")
    else Either.Right(a / b)

// Обработка результата
val result = divide(10, 2).let { either ->
    when (either) {
        is Either.Left -> println("Error: ${either.value}")
        is Either.Right -> println("Result: ${either.value}")
    }
}

Применение: Функциональное программирование, обработка ошибок без исключений

2. State Machine

sealed class ConnectionState {
    object Disconnected : ConnectionState()
    object Connecting : ConnectionState()
    data class Connected(val sessionId: String) : ConnectionState()
    data class Error(val reason: String) : ConnectionState()
}

class NetworkManager {
    fun transition(current: ConnectionState, event: Event): ConnectionState = 
        when (current to event) {
            ConnectionState.Disconnected to Event.Connect -> ConnectionState.Connecting
            ConnectionState.Connecting to Event.Success -> ConnectionState.Connected("session123")
            // ... другие переходы
            else -> current
        }
}

Применение: Конечные автоматы, управление состоянием

3. Command Pattern (CQRS)

sealed interface DatabaseCommand {
    data class Insert(val entity: Entity) : DatabaseCommand
    data class Update(val id: String, val changes: Map<String, Any>) : DatabaseCommand
    data class Delete(val id: String) : DatabaseCommand
    data class Select(val query: Query) : DatabaseCommand
}

fun executeCommand(command: DatabaseCommand): DatabaseResult = when (command) {
    is DatabaseCommand.Insert -> repository.insert(command.entity)
    is DatabaseCommand.Update -> repository.update(command.id, command.changes)
    is DatabaseCommand.Delete -> repository.delete(command.id)
    is DatabaseCommand.Select -> repository.select(command.query)
}

Применение: CQRS, event sourcing, чистая архитектура

4. Spring Boot Response Wrapper

sealed class ApiResponse<out T> {
    data class Success<T>(val data: T, val code: Int = 200) : ApiResponse<T>()
    data class Error<T>(val message: String, val code: Int = 400) : ApiResponse<T>()
    data class Unauthorized<T>(val message: String = "Unauthorized") : ApiResponse<T>()
}

@RestController
class UserController {
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<ApiResponse<UserDto>> {
        return try {
            val user = userService.findById(id)
            ResponseEntity.ok(ApiResponse.Success(user))
        } catch (e: NotFoundException) {
            ResponseEntity.status(404).body(ApiResponse.Error(e.message ?: "Not found", 404))
        }
    }
}

Продвинутые техники

Вложенные sealed классы

sealed class ApiError {
    sealed class NetworkError : ApiError() {
        object NoConnection : NetworkError()
        object Timeout : NetworkError()
        data class HttpError(val code: Int) : NetworkError()
    }
    
    sealed class ParseError : ApiError() {
        object InvalidJson : ParseError()
        data class MissingField(val field: String) : ParseError()
    }
    
    data class UnknownError(val cause: Throwable) : ApiError()
}

// Обработка с вложенной иерархией
fun handleError(error: ApiError) = when (error) {
    is ApiError.NetworkError.NoConnection -> "Check internet connection"
    is ApiError.NetworkError.Timeout -> "Request timeout"
    is ApiError.NetworkError.HttpError -> "HTTP ${error.code}"
    is ApiError.ParseError.InvalidJson -> "Invalid JSON response"
    is ApiError.ParseError.MissingField -> "Missing field: ${error.field}"
    is ApiError.UnknownError -> "Unknown error: ${error.cause.message}"
}

Применение: Иерархическая классификация ошибок

Generics с bounded types

sealed class Validated<out T> {
    data class Valid<T>(val value: T) : Validated<T>()
    data class Invalid(val errors: NonEmptyList<ValidationError>) : Validated<Nothing>()
}

// Ограничение типов
sealed class Resource<out T : Any> {
    object Loading : Resource<Nothing>()
    data class Success<T : Any>(val data: T) : Resource<T>()
    data class Error(val exception: Throwable) : Resource<Nothing>()
}

// Использование
val result: Resource<User> = when {
    isLoading -> Resource.Loading
    hasError -> Resource.Error(exception)
    else -> Resource.Success(user)
}

Применение: Типобезопасная валидация, ресурсы с ограничениями


Лучшие практики для собеседования

1. Exhaustive When

// Компилятор требует обработки всех случаев
fun processState(state: UiState): String = when (state) {
    UiState.Idle -> "Ожидание"
    UiState.Loading -> "Загрузка"
    is UiState.Content -> "Контент: ${state.items.size} элементов"
    is UiState.Error -> "Ошибка: ${state.throwable.message}"
    // else НЕ нужен — sealed гарантирует полноту
}

2. Immutability

// Состояния должны быть неизменяемыми
sealed class TaskState {
    object Pending : TaskState()
    data class InProgress(val progress: Int) : TaskState() // val, не var
    data class Completed(val result: String) : TaskState()
}

3. Meaningful Names

// Плохо
sealed class S { class A : S(); class B : S() }

// Хорошо
sealed class PaymentResult {
    data class Success(val transactionId: String) : PaymentResult()
    data class Declined(val reason: String) : PaymentResult()
    data class NetworkError(val cause: Throwable) : PaymentResult()
}

4. Реальный пример с Spring Boot

sealed class DbResult<out T> {
    data class Success<T>(val value: T) : DbResult<T>()
    data class NotFound(val message: String) : DbResult<Nothing>()
    data class Failed(val exception: Exception) : DbResult<Nothing>()
}

@Service
class UserRepository {
    fun findById(id: Long): DbResult<User> = try {
        val user = database.query(id)
        if (user != null) DbResult.Success(user)
        else DbResult.NotFound("User with id $id not found")
    } catch (e: Exception) {
        DbResult.Failed(e)
    }
}

@RestController
class UserController(private val repo: UserRepository) {
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<*> {
        return when (val result = repo.findById(id)) {
            is DbResult.Success -> ResponseEntity.ok(result.value)
            is DbResult.NotFound -> ResponseEntity.notFound().build()
            is DbResult.Failed -> ResponseEntity.status(500).body(result.exception.message)
        }
    }
}

Производительность

Sealed классы не имеют runtime overhead:

  • sealed - это только constraint на уровне компиляции
  • when выражения компилируются в tableswitch, как в Java enum
  • Нет дополнительных проверок типов во время выполнения
// Компилятор оптимизирует sealed when
sealed class Event
data class Click(val x: Int, val y: Int) : Event()
data class Scroll(val delta: Int) : Event()

fun handle(event: Event) = when (event) {
    is Click -> "Clicked at ${event.x}, ${event.y}"
    is Scroll -> "Scrolled by ${event.delta}"
    // компилируется в tableswitch без runtime checks
}

Ключевые вопросы для интервью

Концептуальные вопросы:

  • Зачем нужны sealed классы? Закрытое множество типов, exhaustive matching, compile-time safety
  • Чем отличаются от enum? Могут содержать данные и сложную логику
  • Когда использовать sealed interface? Множественная реализация, композиция, нужна совместимость с Java

Практические вопросы:

  • Как обрабатывать ошибки с sealed классами? Result pattern вместо исключений
  • Можно ли добавить новый подтип? Только в том же файле/пакете (в зависимости от версии Kotlin)
  • Работают ли с generics? Да, полная поддержка ковариантности

Архитектурные вопросы:

  • Применение в Clean Architecture? Use cases, domain events, результаты операций
  • Интеграция с Spring Boot? Wrapper для API responses, domain events, command pattern
  • Performance considerations? Нет runtime overhead, компилируется в tableswitch

Главное преимущество: Sealed классы обеспечивают compile-time safety и делают код более предсказуемым, что критично для enterprise разработки.