Introducción: por qué es normal confundirse con el asterisco y Any
\n\nSi llevas al menos un par de semanas escribiendo en Kotlin, seguro que te has topado con los genéricos. Y si has mirado el código fuente de la biblioteca estándar o has escrito algo como List<*>, quizás te hayas preguntado: «¿Y por qué no escribir simplemente List<Any>? ¿Acaso el asterisco no significa "cualquier tipo"?»
Esta pregunta es un verdadero éxito en Stack Overflow (miles de visitas). Y no es de extrañar: a primera vista, * y Any parecen intercambiables. Pero en realidad, hay un abismo entre ellos, y si no lo notas, puedes escribir código que no compile o, peor aún, que se rompa silenciosamente en tiempo de ejecución.
En este artículo, aclararemos de una vez por todas en qué se diferencian * (star projection) y Any en los genéricos de Kotlin. Aprenderás cuándo usar cada uno, cómo funciona la varianza y por qué el asterisco no es un "Any", sino un "no sé qué hay ahí, pero me da igual". ¡Allá vamos!
Brevemente sobre genéricos: ¿por qué son necesarios?
\n\nLos genéricos (generalizaciones) nos permiten escribir clases y funciones que funcionan con diferentes tipos sin perder la seguridad de tipos. En lugar de escribir una clase separada para Int, otra para String y otra para User, escribimos una sola clase Box<T>.
Pero, ¿qué hacer si queremos escribir una función que acepte cualquier Box, independientemente de lo que contenga? Ahí es donde aparecen nuestros protagonistas: * y Any.
Any es un tipo concreto (casi como Object)
\n\nEn Kotlin, Any es la raíz de la jerarquía de tipos para todos los tipos no nulos. Si escribes List<Any>, significa: «una lista que contiene elementos de tipo Any». Es decir, puedes poner Int, String, User — cualquier cosa, porque todos heredan de Any.
Un punto importante: List<Any> es un tipo parametrizado concreto. El compilador sabe que el tipo del elemento es Any.
val listOfAny: MutableList<Any> = mutableListOf(1, "hello", 3.14)\nlistOfAny.add(42) // OK\nlistOfAny.add("world") // OK\n\n\nAquí todo es predecible: hemos dicho explícitamente que la lista trabaja con Any. Pero hay un matiz: MutableList<Any> no es MutableList<String>. No puedes pasar una lista de cadenas donde se espera una lista de Any (a menos que uses covarianza, pero de eso hablaremos más adelante).
El asterisco (*) es «no lo sé, pero me da igual»
\n\nEl asterisco, o star projection, es una historia completamente diferente. Cuando escribes List<*>, le dices al compilador: «Esta es una lista de algún tipo, pero no sé cuál exactamente y no me importa». Es una proyección de tipo desconocido.
La diferencia clave: * no es un tipo, sino un wildcard (comodín). Significa que el tipo es desconocido y el compilador impone restricciones sobre lo que puedes hacer con dicho objeto.
fun printLength(list: List<*>) {\n // list.add(1) // ¡Error de compilación! No se puede añadir un elemento\n println(list.size) // OK: se puede conocer el tamaño\n for (item in list) {\n println(item) // OK: item es de tipo Any?\n }\n}\n\n\nObserva: cuando lees elementos de List<*>, estos son de tipo Any? (porque la lista podría contener null). Pero no puedes agregar elementos, porque el compilador no sabe qué tipo se almacena realmente. Esto es una protección contra la violación de la seguridad de tipos.
La principal diferencia en dos palabras
\n\n- \n
List<Any>— es una lista que puede contener