Урок по треку Kotlin 7 мин

Дженерики и вариантность за 7 минут

in, out, use-site вариантность, star projection, ограничения и стирание типов.

Вариантность пугает всех, но на собесе нужен один понятный ответ: почему List<String> можно передать туда, где ждут List<Any>, а MutableList<String> нельзя. Разберёмся.

Дженерики

class Box<T>(val value: T)
fun <T> firstOrDefault(list: List<T>, default: T): T = list.firstOrNull() ?: default

fun <T : Comparable<T>> maxOf3(a: T, b: T, c: T): T = maxOf(a, maxOf(b, c))

fun <T> copyIfValid(x: T) where T : CharSequence, T : Comparable<T> { }

Верхняя граница по умолчанию Any?, поэтому T может оказаться nullable. Чтобы запретить null, пишут <T : Any>.

Инвариантность

По умолчанию дженерики инвариантны: Box<String> не является ни подтипом, ни супертипом Box<Any>, хотя String подтип Any. Почему так нужно:

val strings: MutableList<String> = mutableListOf("a")
val anys: MutableList<Any> = strings   // представим, что можно
anys.add(42)                           // в списке строк оказалось число
val s: String = strings[1]             // ClassCastException

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

Если класс только отдаёт T (возвращает), но никогда не принимает, его можно объявить с out T:

interface Source<out T> { fun next(): T }

val strings: Source<String> = ...
val anys: Source<Any> = strings   // ok

Так устроен List<out E> из stdlib: у него нет add, поэтому List<String> безопасно считать List<Any>. T с out нельзя использовать в параметрах методов (позиция in), компилятор не даст.

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

Если класс только принимает T, его объявляют in T, и подтипизация переворачивается:

interface Comparator<in T> { fun compare(a: T, b: T): Int }

val anyCmp: Comparator<Any> = ...
val strCmp: Comparator<String> = anyCmp   // ok: умеет сравнивать всё, сравнит и строки

Запоминалка из Java: PECS (Producer Extends, Consumer Super). В Kotlin: производитель out, потребитель in.

Вариантность в месте использования

Если класс инвариантный (MutableList, Array), вариантность указывают при использовании, это аналог Java-wildcards:

fun copy(from: Array<out Any>, to: Array<in Any>) {
    for (i in from.indices) to[i] = from[i]
}

Array<out Any> то же, что Array<? extends Object>, MutableList<in Int> то же, что List<? super Integer>.

Star projection

List<*> значит «список чего-то, не знаю чего». Читать можно как Any?, записывать нельзя ничего. Аналог List<?> в Java. Удобно, когда тип не важен: fun printAll(list: List<*>).

Стирание типов

На JVM дженерик-параметры стираются при компиляции: в рантайме List<String> и List<Int> это просто List. Поэтому:

if (x is List<String>) { }   // ошибка: cannot check for erased type
if (x is List<*>) { }        // можно

Обход: inline fun <reified T> (тип подставляется в месте вызова) или передача KClass<T> / Class<T> явно.

Ещё следствие: нельзя перегрузить fun f(list: List<String>) и fun f(list: List<Int>), у них одинаковая сигнатура после стирания. Помогает @JvmName.

Nothing и дженерики

emptyList() возвращает List<Nothing>, а благодаря out и тому, что Nothing подтип всего, его можно присвоить в List<String>. Так же object Empty : Result<Nothing> подходит под любой Result<T>, если Result объявлен с out T.

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

  • Объявить out T и пытаться принимать T в методе.
  • Ожидать, что MutableList<String> подойдёт под MutableList<Any>.
  • Проверять is List<String> в рантайме.
  • Забыть, что T без ограничения может быть null.

Что спросят на собесе

  • Что такое ковариантность и контравариантность, чем out отличается от in?
  • Почему List<String> подтип List<Any>, а MutableList<String> нет?
  • Что такое star projection?
  • Что такое стирание типов и как его обойти?
  • Чем declaration-site вариантность Kotlin лучше wildcards Java?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.