Introducción a las colecciones en Kotlin

Introducción a las colecciones en Kotlin

En general, en programación las colecciones son estructuras o conjuntos de elementos o ítems, y concretamente los tipos de colecciones usadas en Kotlin son:
  • Listas (List): colección ordenada con acceso a sus elementos por el índice o posición de cada uno.
  • Conjuntos (Set): colección de elementos únicos (no repetidos) donde el orden no suele ser relevante.
  • Diccionarios (Map): conjunto de pares del tipo clave-valor donde las claves son únicas y a cada una de ellas se asigna un valor (que pueden ser duplicados), por lo que suelen ser útiles para almacenar conexiones lógicas entre objetos.

Para administrar estas colecciones, la biblioteca estándar de Kotlin proporciona un conjunto completo de herramientas (interfaces, clases y funciones) en el paquete kotlin.collections que, como vimos al hablar de la importación de paquetes, se importa de manera automática y están listas para usar en cualquier archivo Kotlin.

En Kotlin las colecciones se pueden agrupar en dos grandes categorías:
  • Colecciones de solo lectura, con operaciones para acceder a los elementos de la colección, como List, Set y Map.
  • Colecciones mutables, con operaciones de escritura como agregar, eliminar y actualizar sus elementos, como MutableList, MutableSet y MutableMap.

Hay que tener en cuenta que una colección mutable no requiere declararla con var porque las operaciones de escritura modifican los objetos contenidos en la colección sin cambiar la referencia a la colección. Aunque si se intenta reasignar una colección declarada con val se produce un error de compilación:
fun main() {
    val numeros = mutableListOf("uno", "dos", "tres", "cuatro")
    numeros.add("cinco") // añade un elemento de la lista
    //numeros = mutableListOf("seis", "siete") // ERROR
}

En Kotlin la raíz jerárquica de las colecciones es la interface Iterable, que define las operaciones para iterar sobre los elementos de una colección. En el siguiente ejemplo vemos que se declaran dos colecciones de solo lectura (List y Set) que son recorridas utilizando un for, y también una lista mutable a la que se añaden y quitan elementos:
// función con un parámetro que es una colección de ítems de tipo String
fun printItems(coleccion: Collection<String>) {
    // recorre los elementos de la colección
    for (item in coleccion) print("$item ")
    println()
}

// función con un parámetro que es una lista mutable
fun List<String>.soloPalabrasLargas(palabras: MutableList<String>, maxLength: Int) {
    // filtra las palabras por su longitud
    // y las añade a la lista mutable pasada como argumento
    this.filterTo(palabras) { it.length >= maxLength }
    // Set de ítems de tipo String
    val autor = setOf("Miguel", "de", "Cervantes")
    // elimina los elementos de la lista mutable que coinciden con el Set
    palabras -= autor
}

fun main() {
    // List de ítems de tipo String
    val stringList = listOf("uno", "dos", "uno")
    printItems(stringList) // uno dos uno

    // Set de ítems de tipo String
    val stringSet = setOf("uno", "dos", "tres")
    printItems(stringSet) // uno dos tres

    // MutableList de ítems de tipo String (vacía)
    val palabrasLargas = mutableListOf<String>()

    val frase = "Miguel de Cervantes : En un lugar de la Mancha de cuyo nombre no quiero acordarme"
    // List de ítems de tipo String creada a partir de un String con split()
    val fraseList = frase.split(" ")
    // llamada a la función que añade elementos a la MutableList vacía
    fraseList.soloPalabrasLargas(palabrasLargas, 3)
    println(palabrasLargas) // [lugar, Mancha, cuyo, nombre, quiero, acordarme]
}

Listas: List y MutableList

Las listas almacenan elementos en un orden específico y proporcionan acceso indexado a ellos comenzando desde cero (el índice del primer elemento) hasta lastIndex que es igual a list.size - 1.
fun main() {
    val numeros = listOf("uno", "dos", "tres", "cuatro")
    println("Número de elementos: ${numeros.size}")
    println("Tercer elemento: ${numeros.get(2)}")
    println("Cuarto elemento: ${numeros[3]}")
    println("Índice del elemento \"dos\": ${numeros.indexOf("dos")}")
}
Número de elementos: 4
Tercer elemento: tres
Cuarto elemento: cuatro
Índice del elemento "dos": 1

Dos listas se consideran iguales si tienen los mismos tamaños y elementos estructuralmente iguales en las mismas posiciones:
data class Persona(var nombre: String, var edad: Int)

fun main() {
    val persona1 = Persona("Juan", 31)
    val personasList1 = listOf<Persona>(Persona("Paco", 20), persona1, persona1)
    val personasList2 = listOf<Persona>(Persona("Paco", 20), Persona("Juan", 31), persona1)
    println(personasList1 == personasList2) // true
    persona1.edad = 32
    println(personasList1 == personasList2) // false
}

MutableList es una List con operaciones de escritura específicas de lista, como agregar o eliminar un elemento en una posición específica.
fun main() {
    val numeros = mutableListOf(1, 2, 3, 4)
    numeros.add(5)      // agrega un elemento al final
    println(numeros)    // [1, 2, 3, 4, 5]
    numeros.removeAt(1) // elimina un elemento por su índice
    println(numeros)    // [1, 3, 4, 5]
    numeros[0] = 0      // cambia el valor de un elemento por su índice
    println(numeros)    // [0, 3, 4, 5]
    numeros.shuffle()   // desordena la lista
    println(numeros)
}

En algunos aspectos las listas son muy parecidas a los arrays, sin embargo el array define su tamaño en la inicialización y es fijo, mientras que una lista no tiene un tamaño predefinido y puede cambiar como resultado de operaciones de escritura al agregar o eliminar elementos.

En Kotlin, la implementación predeterminada de List es ArrayList, que se puede considerar como un array de tamaño variable.

Conjuntos: Set y MutableSet

Set almacena elementos únicos (no repetidos), generalmente sin orden definido.

Dos conjuntos son iguales si tienen el mismo tamaño y para cada elemento de un conjunto hay un elemento igual en el otro conjunto.
fun main() {
    val numerosSet = setOf(1, 2, 3, 4)
    println("Número de elementos: ${numerosSet.size}")
    // Número de elementos: 4
    val uno = 1
    if (numerosSet.contains(uno)) println("$uno está en $numerosSet")
    // 1 está en [1, 2, 3, 4]

    val numerosReves = setOf(4, 3, 2, 1)
    println("¿Los conjuntos son iguales?: ${numerosSet == numerosReves}") // true
}

Por su parte, MutableSet es un Set que admite operaciones de escritura.

Dos formas de implementar Set son LinkedHashSet (por defecto) y HashSet, con la diferencia de que la primera preserva el orden de inserción de los elementos y por tanto las funciones que se basan en el orden como first() o last() devuelven resultados predecibles en tales conjuntos, mientras que HashSet, al no considerar el orden de los elementos, requiere menos memoria para almacenar la misma cantidad de elementos.
fun main() {
    val numerosSet = setOf(1, 2, 3, 4)  // LinkedHashSet es la implementación por defecto
    val numerosReves = setOf(4, 3, 2, 1)

    println(numerosSet.first() == numerosReves.first()) // false
    println(numerosSet.first() == numerosReves.last())  // true

    val numerosHashSet = HashSet<Int>(3) // declaramos el tamaño inicial
    numerosHashSet.add(1)
    numerosHashSet.add(2)
    numerosHashSet.add(3)
    numerosHashSet.add(4)

    val intSet = hashSetOf<Int>(1,2,3,4)
    val strSet: HashSet<String> = hashSetOf<String>("Kotlin","Java", "Phyton", "C++")
}

Diccionarios: Map y MutableMap

Map almacena pares clave-valor. Las claves son únicas, pero diferentes claves pueden emparejarse con valores iguales. La interfaz de Map proporciona funciones específicas, como el acceso al valor por clave, la búsqueda de claves y valores, etc.
fun checkValor(x: Int) = println("El valor $x está en numerosMap")

fun main() {
    val numerosMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

    println("Todas las claves: ${numerosMap.keys}")
    println("Todos los valores: ${numerosMap.values}")
    if ("key2" in numerosMap) println("Valor de la clave \"key2\": ${numerosMap["key2"]}")

    val valor = 2
    if (valor in numerosMap.values) checkValor(valor)
    if (numerosMap.containsValue(valor)) checkValor(valor) // igual que el anterior
}
Todas las claves: [key1, key2, key3, key4]
Todos los valores: [1, 2, 3, 1]
Valor de la clave "key2": 2
El valor 2 está en numerosMap
El valor 2 está en numerosMap

Dos diccionarios que contienen los pares iguales son iguales sin importar el orden del par:
fun main() {
    val map1 = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)
    val map2 = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3)

    println("${map1 == map2}") // true
}

MutableMap es un Map con operaciones de escritura como por ejemplo agregar un nuevo par clave-valor o actualizar el valor asociado con una clave.
fun main() {
    val numerosMap = mutableMapOf("uno" to 1, "dos" to 2)
    numerosMap.put("tres", 3)
    numerosMap["uno"] = 11

    println(numerosMap) // {uno=11, dos=2, tres=3}
}

La implementación predeterminada de Map es LinkedHashMap, que conserva el orden de inserción de los elementos al iterar sobre el diccioanrio, a diferencia de la implementación alternativa, HashMap, que no almacena información sobre el orden de los elementos.

En posteriores entradas profundizaremos con detalle en las posibilidades de los distintos tipos de colecciones e ilustraremos su uso con ejemplos.

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos