Tipos genéricos en Kotlin

Tipos genéricos en Kotlin

Ya sabemos que Kotlin es un lenguaje de programación fuertemente tipado o de tipado estático, en el que el tipo, si no es explícito, es inferido y verificado. En este contexto, los tipos genéricos son poderosas herramientas que permiten definir clases, métodos y propiedades de manera que se puede acceder a ellos utilizando diferentes tipos. Es común utilizarlos en combinación con las colecciones para crear estructuras cuyos elementos no están limitados a un tipo determinado.

Una clase o un método de tipo genérico se declara con parámetros de tipo (o tipo parametrizado) entre corchetes, y al crear una instancia de esa clase se debe proporcionar el tipo real del argumento (salvo que pueda ser inferido), por ejemplo:
class Caja<T>(t: T) {
    var valor = t
}

fun main() {
    val caja1 = Caja<Int>(1) // val caja: Caja<Int> = Caja<Int>(1)
    val caja2 = Caja(1) // inferencia del tipo
}

El uso de genéricos conlleva una serie de ventajas, como son la seguridad de tipo (la comprobación se realiza en tiempo de compilación para evitar problemas durante la ejecución) y que no requiere la conversión entre tipos ni encasillar el objeto en un tipo determinado.

En el siguiente ejemplo, creamos la clase Persona con un constructor primario con un parámetro tipo Int; creamos dos instancias de la clase pasando en cada una un tipo distinto de argumento (30 y "30"), y en el segundo objeto se genera un error en tiempo de compilación al no haber coincidencia de tipos entre el parámetro y el argumento:
class Persona(edad: Int) {
    var edad: Int = edad

    init {
        this.edad = edad
        println(edad)
    }
}

fun main() {
    var edadInt: Persona = Persona(30)
    var edadString: Persona = Persona("30") // ERROR: Type mismatch: inferred type is String but Int was expected
}

Podemos resolver el problema anterior usando una clase de tipo genérico que es una clase que acepta diferentes tipos de parámetros:
class Persona<T>(edad: T) {
    var edad: T = edad

    init {
        this.edad = edad
        println(edad)
    }
}

fun main() {
    var edadInt: Persona<Int> = Persona<Int>(30)
    var edadString: Persona<String> = Persona<String>("30")
}

Ahora la clase Persona es una clase de tipo genérico y por tanto acepta distintos tipos de argumentos; el parámetro de tipo funciona como un marcador de posición que será reemplazado por el tipo del argumento utilizado cuando se genere una instancia u objeto. En el ejemplo anterior, cada objeto creado reemplaza el tipo T de la clase Persona con Int y String respectivamente.

Otro ejemplo de una clase de tipo genérico:

class Lista<T> {
    private val elementos = mutableListOf<T>()

    fun size(): Int = this.elementos.size

    fun put(elemento: T) {
        this.elementos.add(elemento)
    }

    fun pop(): T = this.elementos.removeAt(this.elementos.lastIndex)

    override fun toString() = this.elementos.toString()
}

fun main() {

    val listaInt = Lista<Int>()
    listaInt.put(1)
    listaInt.put(3)
    listaInt.put(2)
    println(listaInt.size()) // 3
    println(listaInt.pop())  // 2
    println(listaInt)        // [1, 3]
    println(listaInt.size()) // 2

    val listaStr = Lista<String>()
    listaStr.put("Kotlin")
    listaStr.put("Doc")
    listaStr.put("Android")
    println(listaStr.size()) // 3
    println(listaStr.pop())  // Android
    println(listaStr)        // [Kotlin, Doc]
    println(listaStr.size()) // 2
}

Como las clases, las funciones también pueden tener parámetros de tipo con la sintaxis fun <T> nombreFuncion(parametro: tipo<T>). Vamos a ver cómo se aplica el tipo genérico a los métodos con un par de ejemplos:
fun <T> mostrarValor(lista: ArrayList<T>) {
    for (elemento in lista) {
        println(elemento)
    }
}

fun main() {
    val stringLista: ArrayList<String> = arrayListOf<String>("Hola", "Mundo")
    mostrarValor(stringLista)

    val floatLista: ArrayList<Float> = arrayListOf<Float>(10.5f, 5.0f, 25.5f)
    mostrarValor(floatLista)
}

Otro ejemplo de función de tipo genérico:
fun <T> mostrar(lista: List<T>) {
    println(lista.joinToString(separator = ", ", prefix = "<", postfix = ">"))
}

fun main() {
    mostrar(listOf(1, 2, 3, 4, 5))              // <1, 2, 3, 4, 5>
    mostrar(listOf("Kotlin", "Doc", "Android")) // <Kotlin, Doc, Android>
}

También podemos aplicar los genéricos a funciones de extensión para crear funciones de extensión de tipo genérico. Por ejemplo, en el código anterior podemos agregar el método mostrarValor a la clase ArrayList:
fun <T> ArrayList<T>.mostrarValor() {
    for (elemento in this) {
        println(elemento)
    }
}

fun main() {
    val stringLista: ArrayList<String> = arrayListOf<String>("Hola", "Mundo")
    stringLista.mostrarValor()

    val floatLista: ArrayList<Float> = arrayListOf<Float>(10.5f, 5.0f, 25.5f)
    floatLista.mostrarValor()
}

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos