Введение: почему путаница со звёздочкой и Any — это нормально
Если вы пишете на Kotlin хотя бы пару недель, вы наверняка сталкивались с дженериками. А если вы заглядывали в исходники стандартной библиотеки или писали что-то вроде List<*>, то, возможно, задавались вопросом: «А почему бы не написать просто List<Any>? Ведь звёздочка вроде как означает “любой тип”?»
Этот вопрос — настоящий хит на Stack Overflow (тысячи просмотров). И неудивительно: на первый взгляд * и Any кажутся взаимозаменяемыми. Но на самом деле между ними — пропасть, и если её не заметить, можно написать код, который не скомпилируется или, что ещё хуже, молча сломается в рантайме.
В этой статье мы раз и навсегда разберёмся, чем отличаются * (star projection) и Any в дженериках Kotlin. Вы узнаете, когда использовать каждый из них, как работает вариантность и почему звёздочка — это не «Any», а «я не знаю, что там, но мне всё равно». Поехали!
Коротко о дженериках: почему они вообще нужны
Дженерики (обобщения) позволяют нам писать классы и функции, которые работают с разными типами, не теряя при этом типобезопасности. Вместо того чтобы писать отдельный класс для Int, отдельный для String и отдельный для User, мы пишем один класс Box<T>.
Но что делать, если мы хотим написать функцию, которая принимает любой Box, независимо от того, что внутри? Тут и появляются наши герои: * и Any.
Any — это конкретный тип (почти как Object)
В Kotlin Any — это корень иерархии типов для всех non-null типов. Если вы пишете List<Any>, это означает: «список, который содержит элементы типа Any». То есть вы можете положить туда Int, String, User — всё, что угодно, потому что все они наследуются от Any.
Важный момент: List<Any> — это конкретный параметризованный тип. Компилятор знает, что тип элемента — Any.
val listOfAny: MutableList<Any> = mutableListOf(1, "hello", 3.14)
listOfAny.add(42) // OK
listOfAny.add("world") // OK
Здесь всё предсказуемо: мы явно сказали, что список работает с Any. Но есть нюанс: MutableList<Any> — это не MutableList<String>. Вы не можете передать список строк туда, где ожидается список Any (если только не используете ковариантность, но об этом позже).
Звёздочка (*) — это «я не знаю, но мне всё равно»
Звёздочка, или star projection, — это совершенно другая история. Когда вы пишете List<*>, вы говорите компилятору: «Это список какого-то типа, но я не знаю, какого именно, и мне это не важно». Это проекция неизвестного типа.
Ключевое отличие: * — это не тип, а wildcard (знак подстановки). Он означает, что тип неизвестен, и компилятор накладывает ограничения на то, что вы можете делать с таким объектом.
fun printLength(list: List<*>) {
// list.add(1) // Ошибка компиляции! Нельзя добавить элемент
println(list.size) // OK: размер узнать можно
for (item in list) {
println(item) // OK: item имеет тип Any?
}
}
Обратите внимание: когда вы читаете элементы из List<*>, они имеют тип Any? (потому что в списке может быть null). Но вы не можете добавлять элементы, потому что компилятор не знает, какой тип там реально хранится. Это защита от нарушения типобезопасности.
Главное отличие в двух словах
List<Any>— это список, который может содержать элементы любого типа, потому что все типы — подтипыAny. Вы можете и читать, и писать.List<*>— это список какого-то одного конкретного типа, но мы не знаем, какого. Мы можем только читать (какAny?), но не можем писать (кромеnullв некоторых случаях).
Практический пример: почему это важно
Представьте, что у вас есть функция, которая должна принимать список и выводить его элементы. Вы можете написать:
fun printItems(items: List<Any>) {
for (item in items) {
println(item)
}
}
Но если вы попробуете передать List<String>, компилятор выдаст ошибку:
val strings: List<String> = listOf("a", "b", "c")
printItems(strings) // Ошибка! List<String> не является List<Any>
Почему? Потому что дженерики в Kotlin инвариантны по умолчанию. List<String> не является подтипом List<Any>, даже несмотря на то, что String — подтип Any.
А вот если использовать звёздочку:
fun printItemsWildcard(items: List<*>) {
for (item in items) {
println(item)
}
}
printItemsWildcard(strings) // OK! List<String> подходит
Звёздочка говорит: «Мне всё равно, какой там тип, я просто хочу прочитать элементы». И это работает для любого списка.
А как же out? Ковариантность и проекции
Если вы знакомы с ключевыми словами in и out, вы могли заметить, что List<*> ведёт себя как List<out Any>. И это правда: звёздочка — это сокращение для проекции с out.
Для класса interface List<out T> (где T ковариантен) List<*> эквивалентен List<out Any?>. Для класса, где тип инвариантен (например, MutableList<T>), MutableList<*> ведёт себя как MutableList<out Any?> — вы можете только читать, но не писать.
Блоги
Рекомендаций по книгам
Читайте также
Чистый код на JavaScript: принципы и примеры для Node.js
Не сходятся FormControl и Input? Решаем проблему Angular Material
C# и файлы: от новичка до мастера работы с файловой системой
Rust в стартапах: 3 истории успеха и код, который работает
Как задать заголовки в HTTP GET запросе на Go: полный гайд