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

Типизация

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

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)

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

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

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

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

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

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

Функции области видимости (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 -> "пенсионер"
}

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

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

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

Циклы 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 в диапазоне")

Коллекции

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)
    }
}

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

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

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

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

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

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()
    }
}

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

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")
    }
}

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

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 свойства

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 функция

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

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

Коллекции

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))

LinkedList vs ArrayList

В Kotlin нет прямого LinkedList, но можно использовать Java коллекции или понимать различия:

// ArrayList (по умолчанию в listOf/mutableListOf)
val arrayList = mutableListOf(1, 2, 3)  // быстрый доступ по индексу O(1)

// LinkedList через Java
val linkedList = java.util.LinkedList<Int>()  // быстрая вставка/удаление O(1)

// Выбор зависит от паттерна использования:
// ArrayList: частый доступ по индексу
// LinkedList: частые вставки/удаления в середине

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

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"}

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))
}

Когда использовать 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()  // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

// 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 элементов

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

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

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

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

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

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

Лямбды (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]

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"]

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 }

Отличия fold vs reduce:

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

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]

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]

Сравнительная таблица scope-функций

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

Nullable chain и scope-функции

Nullable chain - цепочка безопасных вызовов с использованием ?. и scope-функций.

data class User(val name: String?, val address: Address?)
data class Address(val street: String?, val city: String?)

val user: User? = getUser()

// Классическая проверка
val city = user?.address?.city

// С использованием let для обработки
val result = user?.let { user ->
    user.address?.let { address ->
        address.city?.let { city ->
            "User lives in $city"
        }
    }
}

// Более элегантно с run
val result2 = user?.run {
    address?.run {
        city?.let { "User lives in $it" }
    }
}

// Комбинирование с также
user?.also { println("Processing user: ${it.name}") }
    ?.address?.also { println("Address found") }
    ?.city?.let { println("City: $it") }

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

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

// Практический пример - Comparable<in T>
class Person(val name: String) : Comparable<Person> {
    override fun compareTo(other: Person): Int = name.compareTo(other.name)
}

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
}

Ограничения типов (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
        }
    }
}

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

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

Phantom types

// Типы-маркеры для состояния
sealed class State
object Open : State()
object Closed : State()

class Connection<S : State> private constructor() {
    companion object {
        fun open(): Connection<Open> = Connection()
    }
    
    fun close(): Connection<Closed> = Connection()
    fun send(data: String): Connection<Open> = this  // только для открытых соединений
}

// Использование - компилятор проверяет состояние
val connection = Connection.open()
    .send("data")    // OK - соединение открыто
    .close()
    // .send("data") // ОШИБКА - соединение закрыто

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

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

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)
}

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

  • Инициализация объекта с установкой свойств
  • 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) }
    .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) }

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

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("Создан пользователь") }
    

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 создаёт закрытую иерархию
  • Все наследники должны быть в том же файле
  • Поддерживает 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)
}

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

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)

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

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

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, чистая архитектура


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

Вложенные 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()
    }
}

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

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>()
}

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


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

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()
}

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

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

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

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

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

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

  • Применение в Clean Architecture? Use cases, domain events, результаты операций
  • Интеграция с Spring Boot? DTOs, состояния процессов, обработка команд
  • Performance considerations? Inline классы для оптимизации, object для singleton

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