Вариантность пугает всех, но на собесе нужен один понятный ответ: почему 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?