Site icon AppTractor

Топ-10 вопросов по Kotlin в 2026 году

В 2026 году Kotlin 2.4.0 упрощает разработку благодаря стабилизации новых возможностей и окончательно закрепляет Kotlin Multiplatform в качестве одной из ведущих архитектур для общей логики кроссплатформенных приложений и интеграции локальных моделей искусственного интеллекта.

1. Почему функции-расширения Kotlin разрешаются статически?

Функции-расширения на самом деле не изменяют класс. Они компилируются в статические функции, которым объект-получатель передаётся в качестве первого параметра.

open class Animal
class Dog : Animal()
fun Animal.sound() = "Animal"
fun Dog.sound() = "Dog"
fun printSound(animal: Animal) {
    println(animal.sound())
}
fun main() {
    printSound(Dog())
}

Результат:

Animal

Хотя во время выполнения объект имеет тип Dog, компилятор выбирает Animal.sound(), поскольку тип переменной на этапе компиляцииAnimal.

Совет: методы класса используют рантайм полиморфизм, а функции-расширения — нет.

2. Почему copy() выполняет поверхностное копирование?

Сгенерированная функция copy() копирует ссылки, а не сами объекты, на которые они указывают.

Поверхностная копия объекта — это копия, свойства которой имеют те же ссылки (указывают на те же базовые значения), что и свойства исходного объекта, с которого была создана копия. В результате, при изменении как исходного объекта, так и копии, вы можете также вызвать изменение и другого объекта. Такое поведение отличается от поведения глубокой копии, при которой исходный объект и копия полностью независимы.

data class Address(var city: String)
data class Employee(
    val name: String,
    val address: Address
)
val emp1 = Employee("John", Address("Delhi"))
val emp2 = emp1.copy()
emp2.address.city = "Mumbai"
println(emp1.address.city)

Результат:

Mumbai

Оба объекта ссылаются на один и тот же экземпляр Address.

Чтобы выполнить глубокое копирование:

val emp2 = emp1.copy(
    address = emp1.address.copy()
)

3. В чём разница между == и ===?

Оператор == проверяет структурное равенство и вызывает equals().

Оператор === проверяет ссылочное равенство, то есть указывают ли переменные на один и тот же объект в памяти.

data class User(val name: String)
val u1 = User("Rahul")
val u2 = User("Rahul")
println(u1 == u2)
println(u1 === u2)

Результат:

true
false

4. Зачем появился sealed interface?

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

Запечатанный интерфейс sealed interface тоже ограничивает набор реализаций, однако позволяет классу одновременно наследоваться от другого класса и реализовывать несколько интерфейсов.

sealed interface Payment
open class BaseResponse
class CardPayment :
    BaseResponse(),
    Payment

Такую комбинацию нельзя реализовать с помощью запечатанного класса, поскольку Kotlin разрешает наследоваться только от одного класса.

5. Почему функция-расширение не может переопределить метод класса?

Методы класса всегда имеют приоритет.

class Person {
    fun greet() = "Member"
}
fun Person.greet() = "Extension"
fun main() {
    println(Person().greet())
}

Результат:

Member

Компилятор всегда выбирает настоящий метод класса.

6. Почему последовательности могут быть быстрее коллекций при обработке больших объёмов данных?

Операции над коллекциями выполняются энергично — каждая операция сразу обрабатывает все элементы.

list.filter {}
    .map {}
    .take(5)

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

Последовательности обрабатываются лениво.

list.asSequence()
    .filter {}
    .map {}
    .take(5)
    .toList()

Элементы обрабатываются по одному, что позволяет снизить расход памяти и избежать лишней работы.

7. Зачем нужен fold(), если уже есть reduce()?

Функция reduce() использует первый элемент коллекции как начальное значение аккумулятора и завершается с ошибкой для пустого списка.

listOf<Int>().reduce { acc, i ->
    acc + i
}

Этот код выбросит исключение.

Функция fold() принимает явно заданное начальное значение.

listOf<Int>().fold(0) { acc, i ->
    acc + i
}

Результат:

0

Благодаря этому fold() безопасно работает с пустыми коллекциями.

8. Почему StateFlow всегда передаёт последнее значение?

StateFlow всегда хранит текущее состояние.

private val _state = MutableStateFlow(0)
val state = _state.asStateFlow()

Когда новый подписчик начинает собирать значения, он сразу получает последнее состояние.

В отличие от SharedFlow, StateFlow предназначен именно для представления состояния приложения или пользовательского интерфейса.

9. Почему reified требует inline?

Обычно информация о параметрах обобщённых типов стирается во время выполнения.

fun <T> printType() {
}

Во время выполнения программа не знает, какой конкретный тип скрывается за T.

Модификатор inline позволяет компилятору подставить тело функции непосредственно в место её вызова и заменить T фактическим типом.

inline fun <reified T> printType() {
    println(T::class.simpleName)
}

Теперь вызов:

printType<String>()

выведет:

String

10. Действительно ли val означает неизменяемость?

Нет.

val делает неизменяемой ссылку, но не обязательно сам объект.

val numbers = mutableListOf(1, 2, 3)
numbers.add(4)

Такой код корректен, поскольку сам список остаётся изменяемым.

Но следующий код не скомпилируется:

numbers = mutableListOf()

Причина в том, что переменная numbers объявлена через val, поэтому ей нельзя присвоить другую ссылку.

Для настоящей неизменяемости следует использовать неизменяемые структуры данных или предоставлять наружу интерфейсы только для чтения.

Дополнительный вопрос: что такое контекстные ресиверы в Kotlin и зачем они появились?

Контекстные получатели (Context Receivers) позволяют функции или свойству объявить, что для их вызова в текущей области видимости должны быть доступны один или несколько контекстных объектов.

Вместо явной передачи зависимостей через параметры функции нужный контекст предоставляется неявно.

Без контекстных получателей:

class Logger {
    fun log(message: String) {
        println(message)
    }
}
fun printUser(logger: Logger, name: String) {
    logger.log("User: $name")
}

Каждая функция должна явно принимать параметр Logger.

С контекстными получателями:

class Logger {
    fun log(message: String) {
        println(message)
    }
}
context(Logger)
fun printUser(name: String) {
    log("User: $name")
}
fun main() {
    val logger = Logger()
    with(logger) {
        printUser("Rahul")
    }
}

Теперь функция printUser() автоматически получает доступ к Logger из текущего контекста.

Источник

Exit mobile version