Avatar ·

Разница между `*` и `Any` в дженериках Kotlin: звездочка vs Any

Всем привет! Я начал изучать Kotlin недавно и столкнулся с непонятным моментом в дженериках. В документации я увидел, что можно использовать `*` (star projection) в дженериках, а также можно писать ``. На первый взгляд кажется, что они делают одно и то же — позволяют хранить в коллекции любые типы. Но я чувствую, что между ними есть какая-то важная разница. Например, я пишу такой код: ```kotlin val list1: MutableList = mutableListOf("строка", 42, 3.14) val list2: MutableList<*> = mutableListOf("строка", 42, 3.14) ``` Обе переменные работают, и я могу добавить в них любой элемент. Но если я попробую прочитать элемент и присвоить его переменной с конкретным типом, то возникают странности. Если я пишу: ```kotlin val item1: String = list1[0] // Ошибка компиляции? Или нет? val item2: String = list2[0] // А здесь? ``` Первый вариант компилируется, а второй — нет. Почему так? И в чем вообще разница между `*` и `Any`? Когда что использовать? Объясните, пожалуйста, на пальцах, с примерами. Заранее спасибо! <<>>

Ключевое отличие: * — это неизвестный тип, Any — это конкретный тип

Отличный вопрос! Это одна из самых частых ловушек для новичков в Kotlin. Давайте разберемся раз и навсегда.

Главная идея: MutableList означает "список, который может содержать только элементы типа Any (а Any — это супертип для всех типов в Kotlin, поэтому туда можно положить что угодно)". А MutableList<*> означает "список, тип элементов которого неизвестен". Это принципиально разные вещи.

Практический пример: запись

С MutableList вы можете положить любой элемент:

val listAny: MutableList<Any> = mutableListOf("строка", 42)
listAny.add(3.14)      // ✅ Работает
listAny.add("новая")   // ✅ Работает
listAny.add(true)      // ✅ Работает

А с MutableList<*> вы не можете положить ничего (кроме null):

val listStar: MutableList<*> = mutableListOf("строка", 42)
listStar.add(3.14)      // ❌ Ошибка компиляции!
listStar.add("новая")   // ❌ Ошибка!
listStar.add(null)      // ✅ Только null можно

Почему так? Потому что компилятор не знает, какой конкретный тип у listStar. Может быть, на самом деле это MutableList? Если бы мы добавили туда число, мы бы сломали типы. Поэтому Kotlin запрещает добавление любых элементов (кроме null, который можно добавить в любой список).

Практический пример: чтение

При чтении все наоборот — * дает больше свободы:

val listAny: MutableList<Any> = mutableListOf("строка", 42)
val itemFromAny: Any = listAny[0] // ✅ Всегда Any

val listStar: MutableList<*> = mutableListOf("строка", 42)
val itemFromStar: Any? = listStar[0] // ✅ Можно присвоить Any? (но не Any!)
val itemFromStarString: String? = listStar[0] as? String // ✅ Только с безопасным приведением

При чтении из MutableList<*> вы получаете Any? (так как элемент может быть null, и тип неизвестен). А из MutableList — гарантированно Any (не nullable).

Когда что использовать?

  • Используйте Any, когда вы хотите создать коллекцию, в которую можно класть разные типы, и вы будете активно добавлять туда элементы.
  • Используйте *, когда вы только читаете данные из коллекции, и вам не важно, какой именно тип там хранится. Например, функция, которая печатает все элементы списка:
fun printAll(items: List<*>) { // Лучше, чем List<Any>
    for (item in items) {
        println(item)
    }
}

Здесь List<*> лучше, потому что вы можете передать List, List или List — функция будет работать со всеми. А если бы вы написали List, то передать List не получилось бы (дженерики в Kotlin инвариантны по умолчанию).

Итоговая таблица

  • MutableList<Any> — можно читать (получаем Any) и писать (любые элементы).
  • MutableList<*> — можно читать (получаем Any?), но писать нельзя (кроме null).
  • List<*> — идеально для read-only коллекций, когда тип не важен.

Надеюсь, теперь стало понятнее! Звездочка — это не "любой тип", а "неизвестный тип", и это накладывает ограничения на запись, но расширяет возможности для чтения.

<<>>

Дополнение: вариант с out-проекцией

Коллега выше отлично объяснил разницу. Я добавлю альтернативный взгляд: звездочка * — это синтаксический сахар для out Any?. То есть MutableList<*> эквивалентен MutableList.

Это объясняет, почему мы можем только читать, но не писать: out-проекция означает, что тип используется только на выходе (в return-типах методов), но не на входе (в параметрах методов).

Еще один полезный пример

Представьте, что у вас есть функция, которая принимает Map:

fun printValues(map: Map<String, *>) {
    for ((key, value) in map) {
        println("$key = $value")
    }
}

fun main() {
    val stringMap: Map<String, String> = mapOf("a" to "hello")
    val intMap: Map<String, Int> = mapOf("b" to 42)
    
    printValues(stringMap) // ✅ Работает
    printValues(intMap)    // ✅ Работает
}

Если бы мы написали Map, то передать Map не получилось бы без явного приведения. Поэтому * — это элегантный способ сказать: "мне все равно, какой тип значений, я их только читаю".

В Kotlin принято использовать * для read-only коллекций и Any только когда реально нужно хранить разнородные данные и добавлять их.