Kotlin
Tutorial Kotlin: Tipos genéricos y varianza
Descubre cómo los tipos genéricos y la varianza en Kotlin optimizan tu código. Aprende a aplicar conceptos de covarianza y contravarianza en tus proyectos.
Aprende Kotlin GRATIS y certifícateDefinición y uso de tipos genéricos
En Kotlin, los tipos genéricos permiten definir clases y funciones que pueden operar con cualquier tipo de dato, proporcionando mayor flexibilidad y reutilización del código. Los genéricos ayudan a crear estructuras de datos y algoritmos que funcionan independientemente del tipo específico de los elementos con los que trabajan.
Para declarar una clase genérica, se utilizan parámetros de tipo entre ángulos <
y >
después del nombre de la clase. Por ejemplo:
class Contenedor<T>(private val valor: T) {
fun obtenerValor(): T {
return valor
}
}
En este ejemplo, Contenedor
es una clase genérica con un parámetro de tipo T
. Esto permite crear instancias de Contenedor
con distintos tipos:
val contenedorEntero = Contenedor(10)
val contenedorCadena = Contenedor("Kotlin")
La variable contenedorEntero
es de tipo Contenedor<Int>
, mientras que contenedorCadena
es de tipo Contenedor<String>
. Gracias a los genéricos, el tipo de dato se mantiene seguro en tiempo de compilación, evitando errores de conversión en tiempo de ejecución.
Las funciones genéricas también son posibles en Kotlin. Se declaran añadiendo parámetros de tipo antes del nombre de la función:
fun <T> imprimirElemento(elemento: T) {
println(elemento)
}
Esta función imprimirElemento
puede recibir un argumento de cualquier tipo, lo que la hace muy versátil:
imprimirElemento(5)
imprimirElemento("Hola")
imprimirElemento(3.14)
Los métodos genéricos dentro de clases no genéricas también son útiles. Por ejemplo:
class Calculadora {
fun <T : Number> sumar(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
}
Aquí, el método sumar
acepta parámetros de tipo T
que deben ser subclases de Number
, garantizando que se pueden realizar operaciones numéricas.
Los genéricos en Kotlin son especialmente útiles al trabajar con colecciones. Las interfaces como List<T>
, Set<T>
y Map<K, V>
utilizan genéricos para asegurar la consistencia de los tipos de sus elementos:
val listaStrings: List<String> = listOf("Kotlin", "Java", "Swift")
val mapaEdades: Map<String, Int> = mapOf("Ana" to 28, "Luis" to 34)
Gracias a los genéricos, el compilador puede verificar que solo se añadan elementos del tipo correcto a las colecciones, mejorando la seguridad de tipos.
Es importante tener en cuenta que los genéricos en Kotlin son inertes, es decir, la información de los tipos genéricos no está disponible en tiempo de ejecución debido a la eliminación de tipos de Java. Sin embargo, Kotlin proporciona soluciones como reified
en funciones inline
para acceder al tipo genérico en tiempo de ejecución, aunque esto se trata en temas más avanzados.
El uso adecuado de los tipos genéricos conduce a un código más reutilizable y robusto, facilitando el desarrollo de aplicaciones más mantenibles y evitando errores relacionados con tipos incorrectos.
Varianza: in
y out
La varianza en Kotlin es un concepto clave que define cómo se relacionan los tipos genéricos cuando existe una jerarquía de herencia. En otras palabras, indica si un tipo genérico puede sustituirse por otro tipo de manera segura sin provocar errores de tipo.
En Kotlin, las clases y interfaces genéricas son invariantes por defecto. Esto significa que, aunque C
sea una subclase de D
, List<C>
no es una subclase de List<D>
, ni viceversa. Para controlar esta relación, Kotlin utiliza los modificadores de varianza out
y in
.
El modificador out
se utiliza para declarar covarianza. Una clase genérica declarada con out
en su parámetro de tipo permite que sus subtipos sean sustituidos por su supertypo. Por ejemplo:
interface Fuente<out T> {
fun obtener(): T
}
class Productor : Fuente<String> {
override fun obtener(): String {
return "Producto"
}
}
fun usarFuente(fuente: Fuente<Any>) {
println(fuente.obtener())
}
fun main() {
val fuenteString: Fuente<String> = Productor()
usarFuente(fuenteString)
}
En este ejemplo, Fuente<out T>
es covariante, lo que permite asignar Fuente<String>
a una variable de tipo Fuente<Any>
. Gracias a la palabra clave out
, se garantiza la seguridad en tiempo de compilación al extraer valores del tipo covariante.
Por otro lado, el modificador in
se utiliza para declarar contravarianza. Una clase genérica declarada con in
en su parámetro de tipo permite que sus supertypos sean sustituidos por sus subtipos. Veamos un ejemplo:
interface Sumidero<in T> {
fun enviar(item: T)
}
class Consumidor : Sumidero<Any> {
override fun enviar(item: Any) {
println("Consumiendo $item")
}
}
fun usarSumidero(sumidero: Sumidero<String>) {
sumidero.enviar("Dato")
}
fun main() {
val sumideroAny: Sumidero<Any> = Consumidor()
usarSumidero(sumideroAny)
}
En este caso, Sumidero<in T>
es contravariante, permitiendo que Sumidero<Any>
se pueda utilizar donde se espera un Sumidero<String>
. El modificador in
asegura que es seguro pasar valores al tipo contravariante.
Es importante tener en cuenta que el modificador out
solo permite usar el tipo genérico como tipo de retorno en funciones, mientras que el modificador in
solo permite usar el tipo genérico como parámetro. Intentar usar el tipo genérico de manera contraria resultará en errores de compilación. Por ejemplo:
class Caja<out T>(private val contenido: T) {
fun obtenerContenido(): T {
return contenido
}
// Error: los parámetros no pueden usar T cuando está declarado con 'out'
// fun actualizarContenido(nuevoContenido: T) {
// contenido = nuevoContenido
// }
}
Aquí, al declarar T
con out
, no es posible usar T
como parámetro de función dentro de la clase. Esto garantiza la seguridad de tipos al impedir operaciones que podrían conducir a inconsistencias.
Además de la varianza en la declaración de tipos, Kotlin también permite especificar varianza en el uso de tipos mediante proyecciones de tipo. Si no se tiene control sobre la declaración de una clase genérica, se puede usar in
o out
al consumirla.
La varianza es esencial para trabajar con colecciones y otros tipos genéricos de manera segura y flexible. Comprender cómo y cuándo utilizar los modificadores in
y out
permite escribir código más robusto y evitar errores de tipos que pueden surgir en sistemas de tipos genéricos.
Restricciones en tipos genéricos
En Kotlin, los tipos genéricos ofrecen flexibilidad al permitir que clases y funciones trabajen con cualquier tipo de dato. Sin embargo, en ocasiones es necesario restringir los tipos que pueden utilizarse como argumentos genéricos para garantizar la seguridad y coherencia del código. Estas restricciones se establecen mediante limitaciones de tipos (type constraints), que especifican las condiciones que deben cumplir los tipos genéricos.
Una forma común de restringir un tipo genérico es mediante una cota superior. Esto se logra usando el signo dos puntos :
tras el nombre del parámetro de tipo, seguido del tipo superior que actúa como límite. Por ejemplo:
fun <T : Number> duplicarValor(numero: T): Double {
return numero.toDouble() * 2
}
En esta función, el tipo genérico T
está restringido a ser una subclase de Number
. Esto garantiza que cualquier objeto pasado a duplicarValor
tendrá los métodos y propiedades disponibles en la clase Number
, como toDouble()
. De esta manera, evitamos errores al asegurar que solo los tipos numéricos pueden utilizarse como argumento.
Las restricciones no se limitan solo a clases; también pueden incluir interfaces. Imaginemos que tenemos una interfaz Comestible
:
interface Comestible {
fun comer()
}
Podemos definir una clase genérica restringida a tipos que implementen Comestible
:
class CajaComida<T : Comestible>(private val elemento: T) {
fun consumir() {
elemento.comer()
}
}
Al establecer T : Comestible
, garantizamos que cualquier objeto almacenado en CajaComida
tendrá el método comer()
, lo que permite utilizarlo sin verificar cada vez si el método existe.
Kotlin también permite establecer múltiples restricciones en un tipo genérico utilizando la palabra clave where
. Esto es útil cuando necesitamos que el tipo genérico cumpla con varias condiciones. Por ejemplo:
fun <T> procesarElemento(elemento: T) where T : Comestible, T : Serializable {
elemento.comer()
// Operaciones relacionadas con la serialización
}
En este caso, T
debe ser tanto Comestible
como Serializable
, asegurando que el elemento pasado a procesarElemento
puede ser consumido y también serializado. Las múltiples restricciones nos permiten crear funciones más específicas y seguras.
Es importante destacar que si se utiliza una clase concreta como cota superior, esa clase debe ser el primer límite, seguido de las interfaces. Por ejemplo:
fun <T> transformar(dato: T): String where T : Number, T : Comparable<T> {
return dato.toString()
}
Aquí, T
está limitado a ser una subclase de Number
y también debe implementar Comparable
.
Otra característica relevante es que las restricciones genéricas también se aplican a las clases. Si definimos una clase genérica con una restricción, todas las funciones y propiedades dentro de ella pueden asumir que el tipo genérico cumple con dicha restricción. Por ejemplo:
class GeneradorInforme<T : Any> {
fun generar(dato: T): String {
return dato.toString()
}
}
Al usar T : Any
, especificamos que T
no puede ser un tipo nulo, lo que nos permite trabajar con dato
sin preocuparnos por la nulabilidad.
En situaciones donde necesitamos asegurar que un tipo genérico es una clase con constructor sin parámetros, podemos utilizar una restricción especial con Any
y el operador constructor()
. Sin embargo, en Kotlin, no se permite restringir directamente a tipos con constructores específicos, pero podemos abordar este caso con patrones de diseño o funciones auxiliares.
Las restricciones también son esenciales al trabajar con funciones de extensión genéricas. Si queremos extender un tipo genérico solo cuando cumple cierta condición, aplicamos una restricción:
fun <T : CharSequence> T.imprimirLongitud() {
println("La longitud es ${this.length}")
}
Esta función de extensión solo estará disponible para tipos que implementen CharSequence
, como String
o StringBuilder
. Así, proporcionamos funcionalidades adicionales a tipos específicos, manteniendo la flexibilidad genérica.
Por último, es crucial entender que las restricciones en tipos genéricos permiten aprovechar al máximo el sistema de tipos de Kotlin, ofreciendo un equilibrio entre generalidad y seguridad. Al limitar los tipos genéricos a aquellos que cumplen ciertas condiciones, podemos escribir código más robusto y minimizar errores en tiempo de compilación, mejorando la calidad y mantenibilidad de nuestras aplicaciones.
Funciones y clases genéricas
Las funciones genéricas en Kotlin permiten escribir funciones que pueden operar con diferentes tipos de datos sin duplicar el código para cada tipo. Al utilizar parámetros de tipo genéricos, se aumenta la flexibilidad y reutilización de las funciones.
Para declarar una función genérica, se coloca el parámetro de tipo entre los símbolos <
y >
antes del nombre de la función:
fun <T> identidad(valor: T): T {
return valor
}
En este ejemplo, identidad
es una función genérica que acepta un parámetro valor
de cualquier tipo T
y devuelve un valor del mismo tipo. La flexibilidad de esta función permite utilizarla con distintos tipos:
val numero = identidad(42)
val texto = identidad("Kotlin")
val lista = identidad(listOf(1, 2, 3))
Las clases genéricas permiten definir estructuras de datos que pueden manejar diferentes tipos de elementos manteniendo la seguridad de tipos. Para declarar una clase genérica, se especifica el parámetro de tipo tras el nombre de la clase:
class Par<A, B>(val primero: A, val segundo: B)
Con esta definición, es posible crear instancias de Par
con diferentes combinaciones de tipos:
val parEnteroCadena = Par(1, "Uno")
val parCadenaBooleano = Par("¿Es Kotlin genial?", true)
La seguridad de tipos garantiza que parEnteroCadena.primero
es de tipo Int
y parEnteroCadena.segundo
es de tipo String
, evitando errores en tiempo de ejecución.
Los constructores genéricos también pueden beneficiarse de parámetros de tipo. Por ejemplo, al crear una clase que contiene una lista mutable de elementos genéricos:
class Almacen<T>(vararg items: T) {
private val elementos = items.toMutableList()
fun agregarElemento(elemento: T) {
elementos.add(elemento)
}
fun obtenerElementos(): List<T> {
return elementos
}
}
Al utilizar vararg
en el constructor, se permite pasar varios argumentos del tipo genérico T
. La clase Almacen
puede utilizarse con diferentes tipos:
val almacenEnteros = Almacen(1, 2, 3)
almacenEnteros.agregarElemento(4)
val almacenCadenas = Almacen("A", "B")
almacenCadenas.agregarElemento("C")
Es importante destacar que los parámetros de tipo pueden tener restricciones. Si una función genérica necesita que el tipo cumpla ciertas condiciones, se pueden imponer límites:
fun <T : Comparable<T>> maximo(a: T, b: T): T {
return if (a > b) a else b
}
En este ejemplo, T
debe implementar la interfaz Comparable
, lo que permite comparar los valores utilizando los operadores >
y <
. Así, se asegura que la función maximo
funcione correctamente con tipos comparables.
Las funciones genéricas de extensión amplían las posibilidades al permitir agregar comportamiento genérico a tipos existentes:
fun <T> List<T>.intercambiar(indice1: Int, indice2: Int): List<T> {
val mutableList = this.toMutableList()
val temp = mutableList[indice1]
mutableList[indice1] = mutableList[indice2]
mutableList[indice2] = temp
return mutableList.toList()
}
Esta función de extensión intercambiar
permite intercambiar elementos en una lista de cualquier tipo T
. Su uso es sencillo:
val listaOriginal = listOf(1, 2, 3)
val listaIntercambiada = listaOriginal.intercambiar(0, 2)
La reificación de tipos genéricos es una característica especial en Kotlin que permite acceder al tipo genérico en tiempo de ejecución dentro de funciones inline
. Al marcar un tipo genérico como reified
, se puede utilizar para realizar comprobaciones de tipo:
inline fun <reified T> esDelTipo(objeto: Any): Boolean {
return objeto is T
}
val resultado = esDelTipo<String>("Kotlin") // Devuelve true
La palabra clave reified
proporciona acceso al tipo T
en tiempo de ejecución, lo cual no es posible en funciones genéricas comunes debido al borrado de tipos (type erasure).
Las clases anidadas genéricas permiten combinar genéricos con estructuras internas:
class Contenedor<T>(val valor: T) {
inner class Contenido {
fun obtenerValor(): T {
return valor
}
}
}
La clase interna Contenido
puede acceder al tipo genérico T
de la clase externa Contenedor
. Esto permite crear estructuras más complejas y flexibles:
val contenedor = Contenedor(100)
val contenido = contenedor.Contenido()
val valor = contenido.obtenerValor() // Devuelve 100
La inferencia de tipos de Kotlin facilita el trabajo con funciones y clases genéricas, ya que el compilador puede deducir el tipo genérico a partir del contexto, reduciendo la necesidad de especificar explícitamente el tipo:
val resultado = identidad("Aprendiendo Kotlin") // El compilador infiere que T es String
Al trabajar con clases genéricas especializadas, es posible crear variantes específicas de clases genéricas para tipos concretos, optimizando el rendimiento o añadiendo funcionalidades específicas:
class ListaEnteros : ArrayList<Int>() {
fun sumarTodos(): Int {
return this.sum()
}
}
En este caso, ListaEnteros
es una clase que hereda de ArrayList<Int>
y añade una función para sumar todos los elementos.
Es fundamental comprender que los tipos genéricos en Kotlin proporcionan una manera poderosa de escribir código flexible y seguro. Al utilizar funciones y clases genéricas adecuadamente, se promueve la reutilización del código y se minimizan los errores relacionados con los tipos.
La coherencia en el uso de genéricos es clave para mantener un código limpio y fácil de mantener. Es recomendable nombrar los parámetros de tipo de manera significativa cuando sea posible, especialmente en clases y funciones complejas:
class Mapeador<Entrada, Salida> {
fun mapear(entrada: Entrada): Salida {
// Implementación del mapeo
}
}
Utilizar nombres como Entrada
y Salida
en lugar de T
o R
mejora la legibilidad y comprensión del código.
En resumen, las funciones y clases genéricas son herramientas esenciales en Kotlin que permiten escribir código más generalizado y adaptable, facilitando el desarrollo de aplicaciones robustas y eficientes.
Ejercicios de esta lección Tipos genéricos y varianza
Evalúa tus conocimientos de esta lección Tipos genéricos y varianza con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases genéricas con varianza y restricciones
Introducción a las corutinas
Uso de asincronía con suspend, async y await
Formateo de cadenas texto
Uso de monads y manejo funcional de errores
Declaración y uso de variables y constantes
Uso de la concurrencia funcional con corutinas
Operaciones en colecciones
Uso de clases y objetos en Kotlin
Evaluación Kotlin
Funciones de orden superior y expresiones lambda en Kotlin
Herencia y polimorfismo en Kotlin
Inmutabilidad y datos inmutables
Uso de funciones parciales y currificaciones
Primer programa en Kotlin
Introducción a la programación funcional
Introducción a Kotlin
Uso de operadores y expresiones
Sistema de inventario de tienda
Uso de data classes y destructuring
Composición de funciones
Uso de interfaces y clases abstractas
Simulador de conversión de monedas
Programación funcional y concurrencia
Creación y uso de listas, conjuntos y mapas
Transformación en monads y functors
Crear e invocar funciones
Uso de las estructuras de control
Todas las lecciones de Kotlin
Accede a todas las lecciones de Kotlin y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Kotlin
Introducción Y Entorno
Instalación Y Primer Programa De Kotlin
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Estructuras De Control
Sintaxis
Funciones Y Llamada De Funciones
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Herencia Y Polimorfismo
Programación Orientada A Objetos
Interfaces Y Clases Abstractas
Programación Orientada A Objetos
Data Classes Y Destructuring
Programación Orientada A Objetos
Tipos Genéricos Y Varianza
Programación Orientada A Objetos
Listas, Conjuntos Y Mapas
Estructuras De Datos
Introducción A La Programación Funcional
Programación Funcional
Funciones De Primera Clase Y De Orden Superior
Programación Funcional
Inmutabilidad Y Datos Inmutables
Programación Funcional
Composición De Funciones
Programación Funcional
Monads Y Manejo Funcional De Errores
Programación Funcional
Operaciones Funcionales En Colecciones
Programación Funcional
Transformaciones En Monads Y Functors
Programación Funcional
Funciones Parciales Y Currificación
Programación Funcional
Introducción A Las Corutinas
Coroutines Y Asincronía
Asincronía Con Suspend, Async Y Await
Coroutines Y Asincronía
Concurrencia Funcional
Coroutines Y Asincronía
Evaluación
Evaluación
Certificados de superación de Kotlin
Supera todos los ejercicios de programación del curso de Kotlin y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender qué son los tipos genéricos en Kotlin y cómo se declaran.
- Implementar clases y funciones genéricas para mejorar la reutilización de código.
- Aplicar los conceptos de covarianza y contravarianza utilizando los modificadores
out
ein
. - Manejar restricciones en tipos genéricos para asegurar seguridad de tipos.
- Utilizar reificación en funciones
inline
para acceder a tipos genéricos en tiempo de ejecución. - Optimizar el uso de colecciones genéricas para mantener la consistencia del tipo.