Introducción: por qué los genéricos no dan miedo, sino que son prácticos
\n\nImagine que está escribiendo una clase «Caja». En ella se puede poner cualquier cosa: un libro, una manzana, un teléfono. Pero si la hace para Object, cada vez que saque algo tendrá que hacer as? Book y rezar para no caer en un ClassCastException. ¿Le suena?
Aquí es donde los generics (genéricos) vienen al rescate: un potente mecanismo de tipificación en Kotlin que permite escribir código flexible sin sacrificar la seguridad de tipos. En este artículo, exploraremos los generics en Kotlin de lo simple a lo complejo: clases básicas, funciones, varianza (in/out), tipos reificados y proyecciones de estrella. Aprenderá cómo hacer el código más limpio y al compilador su mejor aliado.
\n\nEl artículo está preparado para el portal educativo pythonlib.ru — su guía en el mundo de los lenguajes de programación modernos.
\n\n1. Fundamentos: ¿qué son los generics en Kotlin?
\n\nGenerics (tipos genéricos) permiten parametrizar clases, interfaces y funciones con un tipo que se especificará al usarlos. Esto evita la duplicación de código y brinda control sobre los tipos en tiempo de compilación.
\n\nVea un ejemplo sencillo:
\n\nclass Box<T>(val item: T)\n\nfun main() {\n val bookBox = Box<String>("Kotlin in Action")\n val intBox = Box<Int>(42)\n // ¡El compilador conoce el tipo!\n val title: String = bookBox.item\n}\n\nPuntos clave:
\n- \n
<T>— parámetro de tipo (puede ser cualquier letra, pero por convención T, R, E, K, V). \n - El tipo se fija al crear el objeto — sin casts en tiempo de ejecución. \n
- Los genéricos solo funcionan en tiempo de compilación (type erasure), pero Kotlin añade «reificación» (más sobre esto después). \n
Funciones genéricas
\nLa tipificación en Kotlin también funciona para funciones. Por ejemplo, una función que devuelve el primer elemento de una lista:
\n\nfun <T> firstItem(list: List<T>): T? = list.firstOrNull()\n\nfun main() {\n val numbers = listOf(1, 2, 3)\n val first: Int? = firstItem(numbers) // T = Int\n println(first) // 1\n}\n\nNote: Kotlin puede inferir el tipo del contexto — no es necesario escribir firstItem<Int>(numbers).
2. Varianza: in y out — cómo no romper la seguridad de tipos
\n\nEl tema más complejo para principiantes es la varianza (variabilidad). Responde a la pregunta: ¿puede List<String> ser un subtipo de List<Any>?
En Java, esto se resuelve con wildcards (? extends / ? super). En Kotlin, con out e in.
out (covarianza) — solo lectura
\nLa marca out significa que el tipo genérico solo puede «entregarse» (devolverse desde métodos). Ejemplo: List<out T>.
interface Source<out T> {\n fun next(): T // OK: T en salida\n // fun add(item: T) // ¡Error! T en entrada prohibido\n}\n\nfun demo(source: Source<String>) {\n val anySource: Source<Any> = source // out lo permite\n}\n\n¿Por qué es seguro? Si solo podemos leer String de la fuente, entonces se puede considerar una fuente de Any.
in (contravarianza) — solo escritura
\nin es lo contrario: el tipo solo puede estar en la entrada. Clásico: Comparable.
interface Comparable<in T> {\n operator fun compareTo(other: T): Int\n}\n\nfun demo(comp: Comparable<Number>) {\n val intComp: Comparable<Int> = comp // in lo permite\n}\n\nSi Comparable<Number> puede comparar cualquier número, entonces ciertamente puede comparar también Int.
in out (invarianza) — por defecto
\nSi no se especifica ni in ni out, el tipo es inv