Generics en Kotlin: para qué sirven y cómo usarlos
Al desarrollar en Kotlin, a menudo te encuentras con la necesidad de escribir código que pueda trabajar con diferentes tipos de datos. En lugar de crear funciones o clases separadas para Int, String, Double y así sucesivamente, puedes usar el mecanismo de Generics (genéricos). Este artículo explicará qué son los Generics en Kotlin, para qué sirven y cómo aplicarlos en la práctica.
¿Qué son los Generics y para qué sirven?
Los Generics (genéricos) son una herramienta que permite escribir clases, interfaces y funciones que trabajan con tipos de datos arbitrarios. Imagina que creas una caja en la que puedes poner cualquier cosa: un libro, un teléfono o comida. En lugar de hacer una caja separada para cada objeto, haces una universal.
Principales ventajas de usar Generics:
- Seguridad de tipos (Type Safety) — el compilador verifica los tipos en tiempo de compilación, lo que previene errores en tiempo de ejecución.
- Reutilización de código — el código escrito una vez funciona con cualquier tipo.
- Legibilidad — el código se vuelve más comprensible, ya que indica explícitamente con qué tipos trabaja.
Sin Generics, tendrías que usar Any y hacer conversiones de tipo constantemente, lo que conlleva errores. Con Generics, el compilador se encarga de la corrección de los tipos.
Fundamentos de Generics en Kotlin: parámetros de tipo
En Kotlin, los Generics se implementan mediante parámetros de tipo. Se indican entre corchetes angulares <>. Normalmente se usan las letras T (Type), E (Element), K (Key), V (Value), pero se puede usar cualquier nombre.
Funciones genéricas
Empecemos con un ejemplo simple: una función que devuelve el argumento que se le pasa:
fun <T> identity(value: T): T { return value}
fun main() { val number: Int = identity(42) val text: String = identity("Hola") println(number) // 42 println(text) // Hola}Aquí, <T> antes del nombre de la función declara el parámetro de tipo. El tipo T se deduce automáticamente del argumento, por lo que no es necesario indicarlo explícitamente.
Clases genéricas
Las clases también pueden ser genéricas. Creemos un contenedor simple:
class Box<T>(private val item: T) { fun getItem(): T { return item }}
fun main() { val stringBox = Box("Libro") val intBox = Box(123)
println(stringBox.getItem()) // Libro println(intBox.getItem()) // 123}La clase Box<T> puede almacenar un objeto de cualquier tipo. Al crear una instancia, indicamos el tipo concreto, y el compilador garantiza que getItem() devolverá exactamente ese tipo.
Restricciones de tipo (Type Bounds)
A veces es necesario restringir los tipos que se pueden usar en un Generic. Por ejemplo, para que una función funcione solo con números. Para ello se usan restricciones (bounds) con la palabra clave :.
Límite superior (Upper Bound)
fun <T : Number> sum(a: T, b: T): Double { return a.toDouble() + b.toDouble()}
fun main() { println(sum(10, 20)) // 30.0 println(sum(3.5, 2.5)) // 6.0 // println(sum("A", "B")) // Error de compilación}Aquí, T : Number significa que el tipo T debe ser una subclase de la clase Number. Int, Double, Float son válidos, pero String no.
Múltiples restricciones
Si se necesitan varias restricciones, usa where:
interface Printable { fun print()}
fun <T>