Базовые конструкции
Типизация
Неявная и явная типизация
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
в Javavar
- изменяемая переменная (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-safetyrun
: доступ через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)
Частые ошибки
-
Путаница с возвращаемыми значениями
// Ошибка: apply возвращает объект, а не результат lambda val length = "Hello".apply { this.length } // вернётся "Hello" // Правильно val length = "Hello".let { it.length } // вернётся 5
-
Неправильный выбор функции
// Избыточно person.let { it.name = "Иван" } // Правильно person.apply { name = "Иван" }
-
Нарушение принципа 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 разработки.