val, var и настоящая неизменяемость. Extension-функции

Вопрос выглядит разминочным, но на нём отсеивают тех, кто выучил синтаксис, а не семантику.

val означает «ссылку нельзя переприсвоить», а не «объект нельзя изменить». Это разные вещи, и путаница между ними — источник настоящих багов, а не только неверных ответов.

Вопрос 1: «Чем val отличается от var? А от immutable?»

Что проверяет интервьюер

Первую половину вопроса знают все. Интересна вторая: понимает ли кандидат, что val ничего не говорит о содержимом объекта.

Развёрнутый ответ

var — переменная, которой можно присвоить новое значение. val — read-only ссылка: присвоить второй раз нельзя. Но объект по этой ссылке вполне может меняться:

val items = mutableListOf("a", "b")
items.add("c")        // разрешено: меняем объект, а не ссылку
// items = mutableListOf()   // ошибка компиляции: val нельзя переприсвоить

class Counter {
    val history = mutableListOf<Int>()   // val, но содержимое изменяемо
}

Второй нюанс: val — это не константа. Свойство с кастомным геттером объявляется через val и возвращает разные значения при каждом обращении:

class Cart(private val items: List<Item>) {
    val total: Int
        get() = items.sumOf { it.price }   // val, но значение меняется вместе с items
}

Настоящая константа — это const val: значение подставляется компилятором в место использования. Ограничения: только на верхнем уровне, в object или в companion object, и только для примитивов и String.

Третий уровень того же вопроса — коллекции. В Kotlin List — это read-only интерфейс, а не immutable-коллекция. Он лишь не объявляет методов изменения. Под ним может лежать тот же ArrayList, который кто-то другой продолжает менять:

val source = mutableListOf(1, 2, 3)
val readOnly: List<Int> = source   // тот же самый объект

source.add(4)
println(readOnly)   // [1, 2, 3, 4] — «неизменяемый» список изменился

Отсюда практический вывод, который стоит озвучить: если наружу отдаётся список, который вы держите внутри, отдавайте копию (toList()) — иначе вызывающий код увидит ваши будущие изменения. Настоящие персистентные коллекции есть в kotlinx.collections.immutable (PersistentList), и в Compose они полезны ещё и как стабильные типы.

Типичные ошибки кандидатов

  • «val — это immutable». Нет: val про ссылку, immutability — про объект.
  • «List нельзя изменить». Нельзя через сам интерфейс, но объект под ним может быть изменяемым.
  • Путают const val и val в companion object: второе — обычное свойство с геттером, к которому из Java обращаются через Companion.

Вопрос 2: «Как работают extension-функции? Можно ли ими переопределить метод класса?»

Что проверяет интервьюер

Знаете ли вы, что extension — это синтаксический сахар над статическим методом, а значит, никакого полиморфизма там нет.

Развёрнутый ответ

Extension-функция не добавляется в класс. Компилятор превращает её в обычную статическую функцию, первым параметром которой становится получатель. Из этого следуют три правила.

1. Extension разрешается статически — по объявленному типу, а не по фактическому:

open class Animal
class Dog : Animal()

fun Animal.name() = "Animal"
fun Dog.name() = "Dog"

fun main() {
    val pet: Animal = Dog()
    println(pet.name())   // Animal — выбор по типу переменной, а не по объекту
}

2. Член класса всегда побеждает extension. Если у класса уже есть метод toString(), ваш fun MyClass.toString() просто никогда не вызовется — компилятор даже предупредит. Переопределить поведение класса extension-функцией невозможно.

3. Extension не имеет доступа к private и protected членам (кроме случая, когда объявлен внутри того же класса или файла для приватных top-level элементов). Это не часть класса, а функция рядом с ним.

Зачем тогда extension нужны? Чтобы добавлять удобные операции к чужим типам, которые вы не можете править — String, Context, классам из библиотек. В Android это стандартный приём:

fun View.showIf(condition: Boolean) {
    visibility = if (condition) View.VISIBLE else View.GONE
}

fun Context.dp(value: Int): Int =
    (value * resources.displayMetrics.density).toInt()

// использование читается как метод самого класса
binding.error.showIf(state.hasError)

Ещё одна деталь на балл сверху: extension можно объявить и для nullable-типа. Тогда внутри this может быть null, и вызывать функцию можно без ?. — именно так устроен fun Any?.toString() в стандартной библиотеке.

Типичные ошибки кандидатов

  • Утверждают, что extension «добавляет метод в класс» — из этого потом следует неверный ответ про полиморфизм.
  • Не знают, что при конфликте выигрывает член класса.
  • Пишут extension на свои же классы там, где хватило бы обычного метода: extension оправдан, когда класс чужой или когда операция не относится к его ответственности.

Как ответить кратко (20–30 секунд)

«val запрещает переприсваивание ссылки, но не запрещает менять объект: val list = mutableListOf() прекрасно принимает add. Настоящая константа — const val, она подставляется на этапе компиляции. List в Kotlin — read-only интерфейс, а не immutable-коллекция: под ним может лежать изменяемый список, поэтому наружу отдаю копию. Extension-функции — это статические функции с получателем первым параметром: они разрешаются по объявленному типу, а не по фактическому, и никогда не перекрывают члены класса. Использую их, чтобы расширять чужие типы вроде View и Context

Проверьте себя
1. Что напечатает код: val a = mutableListOf(1); val b: List<Int> = a; a.add(2); println(b)?
A[1] — b это неизменяемая копия
B[1, 2] — b ссылается на тот же объект, List лишь read-only интерфейс
Cкод не скомпилируется: нельзя присвоить MutableList переменной типа List
DConcurrentModificationException
2. Объявлены fun Animal.name() = "Animal" и fun Dog.name() = "Dog", где Dog наследует Animal. Что вернёт (val pet: Animal = Dog()).name()?
A"Dog" — работает динамическая диспетчеризация
B"Animal" — extension-функции разрешаются статически по объявленному типу
Cошибка компиляции из-за неоднозначности
D"Dog", но только если Animal объявлен как open
3. Что произойдёт, если написать extension-функцию с той же сигнатурой, что и существующий метод класса?
Aextension переопределит метод класса
Bбудет ошибка компиляции
Cвызовется метод класса, extension никогда не сработает
Dповедение зависит от порядка импортов