Saltar al contenido
K
Kotlin: el lenguajeConcepto33 min Básico

Colecciones

Al terminar: Elegir entre List, Set y Map, y transformar colecciones con operaciones como map, filter, groupBy, fold y reduce.

Al terminar esta lección sabrás guardar muchos valores juntos —los eventos de un partido, los equipos de una liga, los goles de cada jugador— y hacerles preguntas con una sola línea: cuántos cumplen una condición, cuánto suman, cómo se agrupan. Son las herramientas con las que dos lecciones más adelante calcularás la tabla de posiciones de Marcador.

Todo se resuelve con unas pocas operaciones sobre listas, mapas y conjuntos, encadenadas. Temas: qué es una colección, List, Map y Set, reduce, fold, groupBy, sortedBy, sumOf y por qué un objeto que cambia da problemas dentro de un Set. map y filter ya los conoces de la lección anterior.

Qué es una colección

Hasta ahora cada variable guardaba un valor: un número, un texto, un jugador. Pero un partido tiene muchos eventos, una liga tiene muchos equipos y una tienda tiene muchos productos. No tiene sentido crear una variable por cada uno, y además no sabes cuántos habrá. Una colección es un valor que guarda muchos valores juntos, y las colecciones de Kotlin traen herramientas para recorrerlos, filtrarlos y calcular con ellos.

En una colección ordenada, cada elemento tiene un índice: el número de su posición. Se cuenta desde 0: el primer elemento está en el índice 0, el segundo en el 1, y así. Por eso una lista de tres elementos llega hasta el índice 2.

Tres tipos de colección

TipoSe pregunta por…RepetidosEn Marcador
List<T>posición: lista[0]sílos eventos de un partido
Set<T>pertenencia: x in conjuntonolos equipos de la tabla, sin repetir
Map<K, V>clave: mapa[clave]valores sí, claves nolos marcadores de cada equipo

T, K y V son huecos que se rellenan con un tipo concreto: List<Int> es una lista de enteros, List<Jugador> una lista de jugadores; en Map<K, V>, K es el tipo de la clave (del inglés key) y V el del valor (value). Así, Map<String, Int> asocia textos con enteros.

Cada tipo viene en dos versiones: la de solo lectura (List, Map, Set), que solo consulta, y la mutable (MutableList, MutableMap, MutableSet), que además añade y quita. «Mutable» quiere decir «que puede cambiar». Se crean con listOf, mapOf, setOf y mutableListOf… Que una lista sea de solo lectura no la hace inmutable del todo; eso es tema de Inmutabilidad y referencias.

Una lista de la compra, en sus dos versiones, con las acciones más usadas:

ejemplo mínimo: una lista de la compra (con comentarios)
fun main() {
    // listOf crea una lista de solo lectura: se puede mirar, no cambiar
    val fija = listOf("pan", "leche", "huevos")
    println(fija[0])           // el primer elemento está en la posición 0
    println(fija[2])           // el tercero, en la posición 2
    println(fija.size)         // size: cuántos elementos hay (es una propiedad: sin paréntesis)
    println("leche" in fija)   // in: ¿está en la lista?
    // fija.add("sal")         // error: una List no tiene add
 
    // mutableListOf crea una lista que también se puede cambiar
    val compra = mutableListOf("pan", "leche")
    compra.add("huevos")       // add: añade al final
    compra.remove("pan")       // remove: quita el elemento que le pases
    println(compra)
    println(compra.size)
}

Al ejecutarlo, imprime:

pan
huevos
3
true
[leche, huevos]
2

Si quitas los // de la línea fija.add("sal"), el programa no compila: una List no tiene add. Esa es la protección que da el tipo de solo lectura.

Un conjunto, con los invitados a una fiesta:

ejemplo mínimo: un conjunto de invitados (con comentarios)
fun main() {
    // setOf crea un conjunto: si repites un elemento, solo queda uno
    val invitados = setOf("Ana", "Luis", "Ana", "Eva")
    println(invitados)                // Ana aparece una sola vez
    println(invitados.size)           // size: cuántos hay (sin contar repetidos)
    println("Luis" in invitados)      // in: ¿está en el conjunto?
    println("Marta" in invitados)
}

Al ejecutarlo, imprime:

[Ana, Luis, Eva]
3
true
false

Cómo decide un Set si algo está repetido (y por qué importa el var)

Un Set (y las claves de un Map) no recorre todos los elementos para buscar uno: usa un número calculado a partir del contenido de cada objeto, el hashCode, para saber dónde guardarlo y dónde buscarlo, igual que un archivador con pestañas por inicial. Para decidir si dos objetos son «el mismo» usa también equals, la función que dice si dos objetos son iguales. Una data class (data class e igualdad) genera las dos a partir de sus propiedades, por eso dos fichas con los mismos datos cuentan como una.

El problema aparece si el contenido cambia (una propiedad var) después de guardar el objeto: su hashCode cambia, pero el objeto sigue en la «pestaña» de antes, y el Set ya no lo encuentra.

ejemplo mínimo: un objeto que cambia dentro de un Set (con comentarios)
// data class con val: dos fichas con los mismos datos cuentan como la misma
data class Ficha(val dorsal: Int)
 
// data class con var: su contenido puede cambiar después de guardarla
data class FichaCambiante(var dorsal: Int)
 
fun main() {
    val repetidas = setOf(Ficha(7), Ficha(7), Ficha(9))
    println(repetidas.size)            // 2: las dos Ficha(7) son la misma
 
    val ficha = FichaCambiante(7)
    val conjunto = mutableSetOf(ficha)
    println(ficha in conjunto)         // true: se guardó con su dorsal 7
    ficha.dorsal = 8                   // cambia el contenido, y con él el hashCode
    println(ficha in conjunto)         // false: el Set busca en la pestaña del 8 y no está
    println(conjunto.size)             // 1: sigue dentro, pero ya no se encuentra
}
2
true
false
1

Por eso en Marcador todo será val y las modificaciones serán copias: un objeto que no cambia nunca se pierde dentro de una colección.

Mapas: buscar por clave

Un mapa guarda parejas: una clave y su valor. El mapa sirve para buscar: le das la clave y te devuelve el valor. Lo que necesitas saber:

  • Pair: una pareja de dos valores. Se crea con to: "Ana" to 20 es la pareja de la clave «Ana» y el valor 20. Sus dos partes se leen con first y second (sin paréntesis: son propiedades de la pareja).
  • Buscar: edades["Luis"] devuelve el valor de esa clave. Si la clave no está, devuelve null (el «no hay nada» de Nulabilidad), así que el tipo de edades["Marta"] es Int?.
  • keys y values: todas las claves y todos los valores.
  • filter sobre un mapa: se queda con las entradas que cumplen. Cada entrada tiene key (la clave) y value (el valor).
ejemplo mínimo: un mapa de edades (con comentarios)
fun main() {
    // "Ana" to 20 crea un Pair: una pareja de dos valores, con first y second
    val pareja = "Ana" to 20
    println(pareja.first)
    println(pareja.second)
 
    // mapOf recibe parejas: la clave a la izquierda de to y el valor a la derecha
    val edades = mapOf("Ana" to 20, "Luis" to 31)
    println(edades["Luis"])            // buscar por clave
    println(edades["Marta"])           // null: esa clave no existe
    println(edades.keys)               // keys: todas las claves
    println(edades.values)             // values: todos los valores
    println(edades.filter { it.value > 25 })   // filter en un mapa: it es cada entrada; it.value, su valor
 
    // for con paréntesis abre cada entrada del mapa en clave y valor
    for ((nombre, edad) in edades) println("$nombre tiene $edad")
}

Al ejecutarlo, imprime:

Ana
20
31
null
[Ana, Luis]
[20, 31]
{Luis=31}
Ana tiene 20
Luis tiene 31

El for ((nombre, edad) in edades) usa la desestructuración de data class e igualdad: abre cada entrada en sus dos partes. Más abajo verás lo mismo dentro de una lambda.

Las operaciones que encadenan

Estas operaciones no modifican la colección original: devuelven una nueva. Las que llevan { … } reciben una lambda y la aplican a cada elemento.

OperaciónQué haceEjemplo con [3, 1, 2, 1]
reduce { total, x -> … }junta todos en un valorreduce { t, g -> t + g } → 7
sortedBy { … }ordena por un criteriosortedBy { it } → [1, 1, 2, 3]
groupBy { … }agrupa en un MapgroupBy { it % 2 } → {1=[3, 1, 1], 0=[2]} (% es el resto de dividir: los impares dan 1 y los pares, 0)
count { … }cuenta los que cumplencount { it == 1 } → 2
sumOf { … }suma un valor de cada unosumOf { it * 10 } → 70
ejemplo mínimo: operaciones sobre una lista (con comentarios)
fun main() {
    val goles = listOf(3, 1, 2, 1)
    println(goles.reduce { total, g -> total + g })     // los junta sumando
    println(goles.sortedBy { it })                      // ordenados de menor a mayor
    println(goles.sumOf { it * 10 })                    // suma lo que dé la lambda por cada elemento
    println(goles.toSet())                              // un Set: sin repetidos
 
    // first() CON paréntesis es una función: de una lista da su primer elemento y de un texto, su primera letra
    // (no confundir con el first SIN paréntesis de un Pair, que es una propiedad)
    println(listOf("Ana", "Ivan").first())             // Ana: el primer elemento de la lista
    println("Ana".first())                              // A: la primera letra, un Char (un solo carácter, no un texto)
    val porInicial = listOf("Ana", "Ivan", "Alba", "Luis").groupBy { it.first() }
    println(porInicial)                                 // un Map: cada inicial con sus nombres
 
    // mapValues: transforma cada valor del mapa y deja las claves; aquí, cada lista pasa a ser su tamaño
    // (en una entrada del mapa, it.value es el valor: aquí, la lista de nombres)
    println(porInicial.mapValues { it.value.size })
 
    // mapValues también puede sumar: cada valor es una lista de parejas y se suma la segunda parte de cada una.
    // La lambda de fuera nombra sus parámetros (_, lista) y la de dentro usa it: es cada pareja de la lista
    val camisetas = listOf("rojo" to 2, "azul" to 1, "rojo" to 4)
    println(camisetas.groupBy { it.first }.mapValues { (_, lista) -> lista.sumOf { it.second } })
}

Al ejecutarlo, imprime:

[6, 2, 4, 2]
[3, 2]
7
[1, 1, 2, 3]
70
[3, 1, 2]
Ana
A
{A=[Ana, Alba], I=[Ivan], L=[Luis]}
{A=2, I=1, L=1}
{rojo=6, azul=1}

Un aviso de reduce: en una colección vacía el programa se para con un error (una excepción: se ve en Excepciones: try, catch y require), porque no tiene un primer elemento del que partir. El mensaje es Empty collection can't be reduced. («una colección vacía no se puede reducir»). Si la colección puede estar vacía, usa sumOf o fold, que recibe el valor inicial entre paréntesis y una lambda con el acumulado y cada elemento:

ejemplo mínimo: fold con un valor inicial (con comentarios)
fun main() {
    val goles = listOf(3, 1, 2, 1)
    // fold(0): empieza con total = 0 y, por cada elemento, calcula el nuevo total
    println(goles.fold(0) { total, g -> total + g })
    // emptyList<Int>() es una lista vacía de enteros (entre los <> va el tipo de lo que guardaría)
    // Con una lista vacía no falla: devuelve el valor inicial
    println(emptyList<Int>().fold(0) { total, g -> total + g })
}
7
0

Lambdas con desestructuración

Si cada elemento de la colección es un Pair, en lugar de escribir it.first y it.second puedes darles nombre poniendo paréntesis en los parámetros:

ejemplo mínimo: desestructuración en una lambda (con comentarios)
fun main() {
    val marcadores = listOf(2 to 1, 0 to 0, 1 to 3)   // (a favor, en contra)
 
    // (aFavor, enContra) abre cada pareja en dos nombres
    val ganados = marcadores.count { (aFavor, enContra) -> aFavor > enContra }
    println(ganados)
}

Al ejecutarlo, imprime:

1

(aFavor, enContra) se lee «abre el par y llama a sus dos partes así». Es lo mismo que count { it.first > it.second }, pero se entiende sin descifrar qué es first.

Sobre un mapa funciona igual, porque cada entrada es una pareja de clave y valor. Y si una de las dos partes no te hace falta, escribe _ en su lugar: «ignórala».

ejemplo mínimo: desestructuración en un mapa, con _ (con comentarios)
fun main() {
    val edades = mapOf("Ana" to 20, "Luis" to 31)
    // (_, edad): se ignora la clave y se llama edad al valor
    println(edades.filter { (_, edad) -> edad > 25 })
}
{Luis=31}

Reto · Ejercicio: las notas de la clase

En el Kotlin Playground, con esta lista de notas, cada una una pareja de alumno y nota: Eva 7, Leo 9, Eva 5, Leo 6 y Sara 8. Imprime, en este orden: la suma de todas las notas; cuántas notas son de 7 o más; y un mapa con la suma de las notas de cada alumno.

La consola debe mostrar:

35
3
{Eva=12, Leo=15, Sara=8}
Ver solución
solución: agrupar y sumar notas
fun main() {
    val notas = listOf("Eva" to 7, "Leo" to 9, "Eva" to 5, "Leo" to 6, "Sara" to 8)
 
    // sumOf: suma un valor de cada pareja (la nota es la segunda parte)
    println(notas.sumOf { it.second })
 
    // count con desestructuración: _ ignora el nombre y nota es la nota
    println(notas.count { (_, nota) -> nota >= 7 })
 
    // groupBy agrupa las parejas por alumno; mapValues cambia cada lista de parejas por la suma de sus notas
    println(notas.groupBy { it.first }.mapValues { (_, lista) -> lista.sumOf { it.second } })
}

Y ahora, lo que sigue

Con todo esto ya tienes las piezas básicas de las colecciones. En la lección siguiente, Secuencias y formato de tablas, aprenderás a encadenar pasos sin listas intermedias y a alinear columnas; y en La tabla de posiciones las usarás con flatMap, groupBy con dos lambdas y los comparadores para calcular la tabla de posiciones de Marcador. En esta lección no cambias nada del proyecto: los ejemplos y el ejercicio se hacen en el Kotlin Playground.

Antes de seguir, comprueba que:

  • El ejercicio de las notas imprime exactamente 35, 3 y {Eva=12, Leo=15, Sara=8} en el Kotlin Playground.
  • Sabes decir cuándo usar List, Set y Map, y qué hace first() con una lista y con un texto.

Comprueba lo que entendiste

6 preguntas · necesitas 5 para completar la lección

  1. 1.Necesitas guardar los equipos de la tabla sin que ninguno salga dos veces. ¿Qué colección encaja?

  2. 2.¿Qué devuelve marcadores.count { (aFavor, enContra) -> aFavor > enContra } cuando marcadores es una lista de Pair<Int, Int>?

  3. 3.¿Qué pasa al hacer emptyList<Int>().reduce { t, x -> t + x }?

  4. 4.Guardas en un MutableSet una data class con una propiedad var y después cambias esa propiedad. ¿Qué pasa al buscar el objeto en el conjunto?

  5. 5.Con val letras = listOf("a", "b", "c"), ¿qué devuelve letras[1]?

  6. 6.¿Cuántos elementos tiene setOf("Ana", "Luis", "Ana", "Eva")?

Faltan 6 de 6