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
| Tipo | Se pregunta por… | Repetidos | En Marcador |
|---|---|---|---|
List<T> | posición: lista[0] | sí | los eventos de un partido |
Set<T> | pertenencia: x in conjunto | no | los equipos de la tabla, sin repetir |
Map<K, V> | clave: mapa[clave] | valores sí, claves no | los 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:
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]
2Si 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:
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
falseCó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.
// 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
1Por 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 conto:"Ana" to 20es la pareja de la clave «Ana» y el valor 20. Sus dos partes se leen confirstysecond(sin paréntesis: son propiedades de la pareja).- Buscar:
edades["Luis"]devuelve el valor de esa clave. Si la clave no está, devuelvenull(el «no hay nada» de Nulabilidad), así que el tipo deedades["Marta"]esInt?. keysyvalues: todas las claves y todos los valores.filtersobre un mapa: se queda con las entradas que cumplen. Cada entrada tienekey(la clave) yvalue(el valor).
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 31El 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ón | Qué hace | Ejemplo con [3, 1, 2, 1] |
|---|---|---|
reduce { total, x -> … } | junta todos en un valor | reduce { t, g -> t + g } → 7 |
sortedBy { … } | ordena por un criterio | sortedBy { it } → [1, 1, 2, 3] |
groupBy { … } | agrupa en un Map | groupBy { 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 cumplen | count { it == 1 } → 2 |
sumOf { … } | suma un valor de cada uno | sumOf { it * 10 } → 70 |
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:
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
0Lambdas 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:
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».
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
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,3y{Eva=12, Leo=15, Sara=8}en el Kotlin Playground. - Sabes decir cuándo usar
List,SetyMap, y qué hacefirst()con una lista y con un texto.
Comprueba lo que entendiste
6 preguntas · necesitas 5 para completar la lección
1.Necesitas guardar los equipos de la tabla sin que ninguno salga dos veces. ¿Qué colección encaja?
2.¿Qué devuelve
marcadores.count { (aFavor, enContra) -> aFavor > enContra }cuandomarcadoreses una lista dePair<Int, Int>?3.¿Qué pasa al hacer
emptyList<Int>().reduce { t, x -> t + x }?4.Guardas en un
MutableSetunadata classcon una propiedadvary después cambias esa propiedad. ¿Qué pasa al buscar el objeto en el conjunto?5.Con
val letras = listOf("a", "b", "c"), ¿qué devuelveletras[1]?6.¿Cuántos elementos tiene
setOf("Ana", "Luis", "Ana", "Eva")?