Generics en Kotlin: para qué sirven y cómo usarlos en la práctica
Los Generics (genéricos) son una de las características clave del lenguaje Kotlin que permite escribir código más flexible, type-safe y reutilizable. Si alguna vez has escrito List<String> o Map<Int, User>, ya te has encontrado con los genéricos. Pero, ¿para qué sirven realmente y cómo sacarles el máximo partido? En este artículo analizaremos la teoría y pasaremos a la práctica con ejemplos reales.
El problema que resuelven los Generics
Imagina que necesitas escribir una función que devuelva el primer elemento de una lista. Sin genéricos, tendrías que escribir una función separada para cada tipo de dato:
fun getFirstString(list: List<String>): String? = list.firstOrNull()fun getFirstInt(list: List<Int>): Int? = list.firstOrNull()fun getFirstUser(list: List<User>): User? = list.firstOrNull()Esto provoca duplicación de código. Usar Any resolvería el problema, pero perderías información sobre el tipo en tiempo de compilación:
fun getFirst(list: List<Any>): Any? = list.firstOrNull()
// Habría que hacer casting de tiposval name: String? = getFirst(strings) as? StringLos Generics resuelven este problema de forma elegante:
fun <T> getFirst(list: List<T>): T? = list.firstOrNull()
// ¡El compilador conoce el tipo!val name: String? = getFirst(listOf("Alice", "Bob"))val age: Int? = getFirst(listOf(25, 30))Ahora una sola función funciona con cualquier tipo, manteniendo total type-safety.
Fundamentos de Generics en Kotlin: sintaxis y declaración
Los genéricos se declaran usando corchetes angulares <T>. La letra T es solo una convención; se pueden usar cualquier nombre: E (elemento), K (clave), V (valor).
Clases Genéricas
class Box<T>(val value: T) { fun getContent(): T = value}
fun main() { val stringBox = Box("Hello") val intBox = Box(42) println(stringBox.getContent()) // Hello println(intBox.getContent()) // 42}Funciones Genéricas
fun <T> printItem(item: T) { println("Item: $item")}
// Se puede especificar el tipo explícitamenteprintItem<String>("Kotlin")// O confiar en la inferencia de tiposprintItem(100)Interfaces Genéricas
interface Repository<T> { fun getById(id: Int): T fun save(item: T): Boolean}
class UserRepository : Repository<User> { override fun getById(id: Int): User { /* ... */ } override fun save(item: User): Boolean { /* ... */ }}Varianza: in, out y el asterisco *
Una de las características más potentes de Kotlin es la varianza. Responde a la pregunta: ¿puede List<String> ser un subtipo de List<Any>?
Covarianza (out)
Usa out cuando tu tipo genérico solo se devuelve (producer). Por ejemplo, List<T> es covariante:
// Puedes asignar List<String> a una variable List<Any>val strings: List<String> = listOf("a", "b")val anyList: List<Any> = strings // ¡OK!
// Pero no puedes agregar elementos// anyList.add(1) // ¡Error de compilación!Ejemplo propio:
class Producer<out T>(private val value: T) { fun produce(): T = value}
fun main() { val stringProducer: Producer<String> = Producer("Hello") val anyProducer: Producer<Any> = stringProducer // ¡Funciona! println(anyProducer.produce())}Contravarianza (in)
Usa in cuando el tipo solo se recibe (consumer). Por ejemplo, Comparable<T>:
Blogs
Book Recommendations
Leer también
WebSocket en Kotlin: aplicaciones en tiempo real de la A a la Z
Generics en C++: plantillas, para qué sirven y cómo usarlas
@Directive vs @Component en Angular: ¿cuál es la diferencia y qué elegir para un principiante en 2024?
Diferencia entre fechas en PHP: 5 enfoques de lo simple a lo profesional
Seguridad de aplicaciones C++: top 10 vulnerabilidades en la práctica