Colecciones en Kotlin

1. Antes de comenzar

En este codelab, aprenderás más sobre las colecciones y sobre lambdas y funciones de orden superior en Kotlin.

Requisitos previos

  • Conocimientos básicos sobre conceptos de Kotlin, tal como se presentaron en los codelabs anteriores
  • Estar familiarizado con el uso del Playground de Kotlin a efectos de crear y editar programas de Kotlin

Qué aprenderás

  • Cómo trabajar con colecciones, incluidos conjuntos y mapas
  • Conceptos básicos sobre lambdas
  • Conceptos básicos sobre las funciones de orden superior

Requisitos

2. Más información sobre las colecciones

Una colección es un grupo de elementos relacionados, como una lista de palabras o un conjunto de registros de empleados. La colección puede tener los elementos ordenados o desordenados, y los elementos pueden ser únicos o no. Ya aprendiste un tipo de colección: las listas. Las listas tienen un orden de los elementos, pero no es necesario que estos sean únicos.

Al igual que con las listas, Kotlin distingue entre colecciones inmutables y mutables. Kotlin ofrece numerosas funciones para agregar o borrar elementos y visualizar y manipular colecciones.

Cómo crear una lista

En esta tarea, repasarás cómo crear una lista de números y ordenarlos.

  1. Abre el Playground de Kotlin.
  2. Reemplaza cualquier código con este código:
fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
}
  1. Para ejecutar el programa, presiona la flecha verde y observa los resultados que aparecen:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
  1. La lista contiene 10 números del 0 al 9. Algunos de los números aparecen más de una vez, mientras que otros no aparecen.
  2. El orden de los elementos en la lista es importante: el primer elemento es 0, el segundo elemento 3, y así sucesivamente. Los elementos permanecerán en ese orden, a menos que los cambies.
  3. Recuerda de los codelabs anteriores que las listas tienen muchas funciones integradas, como sorted(), que muestra una copia de la lista en orden ascendente. Después de println(), agrega una línea a tu programa para imprimir una copia ordenada de la lista:
println("sorted: ${numbers.sorted()}")
  1. Vuelve a ejecutar el programa y observa los resultados:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]

Con los números ordenados, es más fácil ver cuántas veces aparece cada número en la lista o si no aparece.

Más información sobre los conjuntos

Otro tipo de colección en Kotlin es el conjunto. Es un grupo de elementos relacionados, pero, a diferencia de una lista, no puede haber duplicados y el orden no importa. Un elemento puede estar ubicado en el conjunto o no, pero si está en el conjunto, solo hay una copia. Esto es similar al concepto matemático de un conjunto. Por ejemplo, hay un conjunto de libros que leíste. Leer un libro varias veces no cambia el hecho de que se encuentra en el conjunto de libros que leíste.

  1. Agrega estas líneas a tu programa para convertir la lista en un conjunto:
val setOfNumbers = numbers.toSet()
println("set:    ${setOfNumbers}")
  1. Ejecuta el programa y observa los resultados:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]
set:    [0, 3, 8, 4, 5, 9, 2]

El resultado tiene todos los números de la lista original, pero cada uno solo aparece una vez. Ten en cuenta que están en el mismo orden que en la lista original, pero ese orden no es significativo para un conjunto.

  1. Para definir un conjunto mutable y un conjunto inmutable, y, luego, inicializarlos con el mismo conjunto de números, pero en un orden diferente, agrega estas líneas:
val set1 = setOf(1,2,3)
val set2 = mutableSetOf(3,2,1)
  1. Agrega una línea para imprimir si son iguales:
println("$set1 == $set2: ${set1 == set2}")
  1. Ejecuta el programa y observa los nuevos resultados:
[1, 2, 3] == [3, 2, 1]: true

Aunque uno es mutable y el otro no, y tienen los elementos en un orden diferente, se consideran iguales porque contienen exactamente el mismo conjunto de elementos.

Una de las operaciones principales que puedes realizar en un conjunto es verificar si un elemento determinado está en el conjunto o no con la función contains(). Ya viste contains(), pero lo usaste en una lista.

  1. Agrega esta línea a tu programa para imprimir si 7 está en el conjunto:
println("contains 7: ${setOfNumbers.contains(7)}")
  1. Ejecuta el programa y observa los resultados adicionales:
contains 7: false

También puedes probar con un valor incluido en el conjunto.

Todo el código anterior:

fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
    println("sorted: ${numbers.sorted()}")
    val setOfNumbers = numbers.toSet()
    println("set:    ${setOfNumbers}")
    val set1 = setOf(1,2,3)
    val set2 = mutableSetOf(3,2,1)
    println("$set1 == $set2: ${set1 == set2}")
    println("contains 7: ${setOfNumbers.contains(7)}")
}

Al igual que con los conjuntos matemáticos, en Kotlin también puedes realizar operaciones como la intersección (∩) o la unión (∪) de dos conjuntos usando intersect() o union().

Más información sobre los mapas

El último tipo de colección que aprenderás en este codelab es un mapa o diccionario. Un mapa es un conjunto de pares clave-valor, diseñados para facilitar la búsqueda de un valor según una clave determinada. Las claves son únicas y cada una se mapea a un solo valor, pero los valores pueden tener duplicados. Los valores de un mapa pueden ser cadenas, números u objetos, o bien otra colección, como una lista o un conjunto.

b55b9042a75c56c0.png

Un mapa resulta útil cuando tienes pares de datos y puedes identificar cada par según su clave. La clave se mapea al valor correspondiente.

  1. En el Playground de Kotlin, reemplaza todo el código con este código que crea un mapa mutable para almacenar los nombres de las personas y sus edades:
fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    println(peopleAges)
}

Esto crea un mapa mutable de String (clave) a Int (valor), inicializa el mapa con dos entradas y, luego, imprime los elementos.

  1. Ejecuta el programa y observa los resultados:
{Fred=30, Ann=23}
  1. Para agregar más entradas al mapa, puedes usar la función put() y pasar la clave y el valor:
peopleAges.put("Barbara", 42)
  1. También puedes usar una notación abreviada para agregar entradas:
peopleAges["Joe"] = 51

A continuación, se detalla todo el código anterior:

fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    peopleAges.put("Barbara", 42)
    peopleAges["Joe"] = 51
    println(peopleAges)
}
  1. Ejecuta el programa y observa los resultados:
{Fred=30, Ann=23, Barbara=42, Joe=51}

Como se indicó anteriormente, las claves (nombres) son únicas, pero los valores (edad) pueden tener duplicados. ¿Qué crees que ocurrirá si intentas agregar un elemento usando una de las mismas claves?

  1. Antes del println(), agrega esta línea de código:
peopleAges["Fred"] = 31
  1. Ejecuta el programa y observa los resultados:
{Fred=31, Ann=23, Barbara=42, Joe=51}

La clave "Fred" no se vuelve a agregar, pero el valor al que se mapea se actualiza a 31.

Como puedes ver, los mapas son útiles como una forma rápida de asignar claves a valores de tu código.

3. Cómo trabajar con colecciones

Aunque tienen cualidades distintas, los diferentes tipos de colecciones comparten muchos comportamientos. Si son mutables, puedes agregar o quitar elementos. Puedes enumerar todos los elementos, buscar un elemento en particular o, a veces, convertir un tipo de colección en otro. Anteriormente, realizaste una conversión de List en Set, con toSet(). A continuación, incluimos algunas funciones útiles para trabajar con colecciones.

forEach

Supongamos que deseas imprimir los elementos en peopleAges e incluir el nombre de la persona y su edad. Por ejemplo, "Fred is 31, Ann is 23,...", etc. Aprendiste sobre bucles for en un codelab anterior, por lo que puedes escribir un bucle con for (people in peopleAges) { ... }.

Sin embargo, enumerar todos los objetos de una colección es una operación común, por lo que Kotlin proporciona forEach(), que revisa todos los elementos y realiza una operación en cada uno.

  1. En el playground, agrega este código después de println():
peopleAges.forEach { print("${it.key} is ${it.value}, ") }

Es similar al bucle for, pero un poco más compacto. En lugar de especificar una variable para el elemento actual, forEach usa el identificador especial it.

Ten en cuenta que no tuviste que agregar paréntesis cuando llamaste al método forEach(). Pasa el código entre llaves {}.

  1. Ejecuta el programa y observa los resultados adicionales:
Fred is 31, Ann is 23, Barbara is 42, Joe is 51,

Eso está muy cerca de lo que quieres, pero hay una coma adicional al final.

Convertir una colección en una string es una operación común, y ese separador adicional al final también es un problema común. Aprenderás a solucionarlo en los próximos pasos.

map

La función map() (que no debe confundirse con una colección de mapa o diccionario anterior) aplica una transformación a cada elemento de una colección.

  1. En tu programa, reemplaza la sentencia forEach con esta línea:
println(peopleAges.map { "${it.key} is ${it.value}" }.joinToString(", ") )
  1. Ejecuta el programa y observa los resultados adicionales:
Fred is 31, Ann is 23, Barbara is 42, Joe is 51

Tiene el resultado correcto y no hay coma adicional. Analiza la línea en detalle.

  • peopleAges.map aplica una transformación a cada elemento de peopleAges y crea una colección nueva de los elementos transformados.
  • La parte entre llaves {} define la transformación que se aplica a cada elemento. La transformación toma un par clave-valor y lo transforma en una string, por ejemplo, <Fred, 31> se convierte en Fred is 31.
  • joinToString(", ") agrega cada elemento de la colección transformada a una string, separada por ,, y sabe que no lo agrega al último elemento.
  • Todo esto funciona con . (operador de puntos), como lo hiciste con llamadas a funciones y accesos a propiedades en codelabs anteriores.

filter

Otra operación común con las colecciones es encontrar los elementos que coincidan con una condición específica. La función filter() muestra los elementos de una colección que coinciden con una expresión.

  1. Después de println(), agrega estas líneas:
val filteredNames = peopleAges.filter { it.key.length < 4 }
println(filteredNames)

Nuevamente, la llamada a filter no necesita paréntesis, y it hace referencia al elemento actual en la lista.

  1. Ejecuta el programa y observa los resultados adicionales:
{Ann=23, Joe=51}

En este caso, la expresión obtiene la longitud de la clave (un String) y comprueba si es menor que 4. Los elementos que coincidan, que tengan un nombre con menos de 4 caracteres, se agregarán a la colección nueva.

El tipo que se mostró cuando aplicaste el filtro a un mapa es un mapa nuevo (LinkedHashMap). Puedes realizar procesamientos adicionales en el mapa o convertirlo a otro tipo de colección, como una lista.

4. Más información sobre lambdas y funciones de orden superior

Lambdas

Revisemos este ejemplo anterior:

peopleAges.forEach { print("${it.key} is ${it.value}") }

Hay una variable (peopleAges) con una función (forEach) a la que se llama. En lugar de los paréntesis que aparecen luego del nombre de la función con los parámetros, verás código entre llaves {} después del nombre de la función. El mismo patrón aparece en el código que usa las funciones map y filter del paso anterior. Se llama a la función forEach en la variable peopleAges y se usa el código entre llaves.

Es como escribir una función pequeña en las llaves, pero no hay un nombre de función. Esta idea, una función sin nombre que se puede usar de inmediato como expresión, es un concepto muy útil llamado expresión lambda o simplemente lambda.

Esto conduce a un tema importante sobre cómo puedes interactuar con las funciones de manera potente con Kotlin. Puedes almacenar funciones en variables y clases, pasar funciones como argumentos y también mostrar funciones. Puedes tratarlas como lo harías con variables de otros tipos, como Int o String.

Tipos de funciones

Para habilitar este tipo de comportamiento, Kotlin tiene los llamados tipos de funciones, en los que puedes definir un tipo específico de función según sus parámetros de entrada y valor que aparece. Se muestra en el siguiente formato:

Ejemplo de tipo de función: (Int) -> Int

Una función con el tipo de función anterior debe tomar un parámetro del tipo Int y mostrar un valor de tipo Int. En notación de tipos de funciones, los parámetros se indican entre paréntesis (separados por comas si hay varios parámetros). A continuación, hay una flecha -> seguida del tipo de datos que se muestra.

¿Qué tipo de función cumpliría con estos criterios? Podrías tener una expresión lambda que triplica el valor de una entrada de número entero, como se muestra a continuación. Para la sintaxis de una expresión lambda, los parámetros aparecen primero (destacados en el cuadro rojo), seguidos de la flecha de función y luego del cuerpo de la función (destacado en el cuadro púrpura). La última expresión de la lambda es el valor que se muestra.

252712172e539fe2.png

Incluso podrías almacenar una lambda en una variable, como se muestra en el siguiente diagrama. La sintaxis es similar a la forma en que declaras una variable de un tipo básico de datos, como un Int. Observa el nombre de la variable (cuadro amarillo), el tipo de variable (cuadro azul) y el valor de la variable (cuadro verde). La variable triple almacena una función. Su tipo es un tipo de función (Int) -> Int, y el valor es una expresión lambda { a: Int -> a * 3}.

  1. Prueba este código en el playground. Define y llama a la función triple pasando un número como 5. 4d3f2be4f253af50.png
fun main() {
    val triple: (Int) -> Int = { a: Int -> a * 3 }
    println(triple(5))
}
  1. El resultado debería ser el siguiente:
15
  1. Dentro de las llaves, puedes omitir la declaración explícita del parámetro (a: Int), omitir la flecha de función (->) y solo tener el cuerpo de la función. Actualiza la función triple declarada en la función main y ejecuta el código.
val triple: (Int) -> Int = { it * 3 }
  1. El resultado debería ser el mismo, pero ahora tu lambda está escrita de una forma más concisa. Para obtener más ejemplos de lambdas, consulta este recurso.
15

Funciones de orden superior

Ahora que comienzas a ver la flexibilidad para manipular funciones en Kotlin, analicemos otra idea muy poderosa: una función de orden superior. Esto significa simplemente pasar una función (en este caso, una lambda) a otra función o mostrar una función desde otra.

Resulta que las funciones map, filter y forEach son ejemplos de funciones de orden superior porque todas toman una función como parámetro. (En la lambda pasada a esta filter de orden superior, es posible omitir el parámetro único y el símbolo de la flecha, además de usar el parámetro it).

peopleAges.filter { it.key.length < 4 }

Este es un ejemplo de una función de orden superior: sortedWith().

Si deseas ordenar una lista de strings, puedes usar el método integrado sorted() para las colecciones. Sin embargo, si deseas ordenar la lista por la longitud de las strings, debes escribir código para obtener la longitud de dos strings y compararlas. Kotlin te permite pasar una lambda al método sortedWith() para hacerlo.

  1. En el playground, crea una lista de nombres y, luego, imprime los datos ordenados por nombre con el siguiente código:
fun main() {
    val peopleNames = listOf("Fred", "Ann", "Barbara", "Joe")
    println(peopleNames.sorted())
}
  1. Ahora imprime la lista ordenada por la longitud de los nombres pasando una lambda a la función sortedWith(). La lambda debe tener dos parámetros del mismo tipo y mostrar un Int. Agrega esta línea de código después de la sentencia println() en la función main().
println(peopleNames.sortedWith { str1: String, str2: String -> str1.length - str2.length })
  1. Ejecuta el programa y observa los resultados:
[Ann, Barbara, Fred, Joe]
[Ann, Joe, Fred, Barbara]

La lambda que se pasa a sortedWith() tiene dos parámetros: str1, que es String, y str2, que es String. A continuación, verás la flecha de función, seguida del cuerpo de la función.

7005f5b6bc466894.png

Recuerda que la última expresión en la lambda es el valor que se muestra. En este caso, muestra la diferencia entre la longitud de la primera string y la longitud de la segunda, que es un Int. Coincide con lo que se necesita para ordenar: si str1 es menor que str2, mostrará un valor inferior a 0. Si str1 y str2 tienen la misma longitud, se mostrará 0. Si str1 es mayor que str2, se mostrará un valor mayor que 0. Si realiza una serie de comparaciones entre dos Strings a la vez, la función sortedWith() da como resultado una lista donde los nombres estarán en orden de longitud ascendente.

OnClickListener y OnKeyListener en Android

Haciendo una conexión con lo que aprendiste en Android hasta ahora, usaste lambdas en codelabs anteriores, por ejemplo, cuando configuraste un objeto de escucha de clics para el botón en la app de Tip Calculator.

calculateButton.setOnClickListener{ calculateTip() }

Usar una lambda para configurar el objeto de escucha de clics es una opción conveniente. A continuación, se muestra la forma larga de escribir el código anterior en comparación con la versión abreviada. No necesitas entender todos los detalles de la versión larga, pero puedes notar algunos patrones entre las dos versiones.

29760e0a3cac26a2.png

Observa cómo la lambda tiene el mismo tipo de función que el método onClick() en OnClickListener (toma en un argumento View y muestra Unit, lo que significa que no se muestra un valor).

La versión abreviada del código es posible debido a algo que se denomina conversión de SAM (método abstracto único) en Kotlin. Kotlin convierte la lambda en un objeto OnClickListener que implementa el metodo abstracto único onClick(). Solo debes asegurarte de que el tipo de función lambda coincida con el tipo de función de la función abstracta.

Como el parámetro view nunca se usa en la lambda, se puede omitir. Luego, tenemos el cuerpo de la función en la expresión lambda.

calculateButton.setOnClickListener { calculateTip() }

Estos conceptos son complejos, así que ten paciencia, ya que te llevará tiempo y experiencia incorporarlos. Veamos otro ejemplo. Recuerda cuando estableciste un objeto de escucha de claves en el campo de texto "Cost of service" de la calculadora de propinas para que el teclado en pantalla pueda ocultarse cuando se presionaba la tecla Intro.

costOfServiceEditText.setOnKeyListener { view, keyCode, event -> handleKeyEvent(view, keyCode) }

Cuando buscas OnKeyListener, el método abstracto tiene los siguientes parámetros onKey(View v, int keyCode, KeyEvent event) y muestra una Boolean. Debido a las conversiones de SAM en Kotlin, puedes pasar una lambda a setOnKeyListener(). Solo debes asegurarte de que la expresión lambda tenga el tipo de función (View, Int, KeyEvent) -> Boolean.

A continuación, se muestra un diagrama de la expresión lambda usada anteriormente. Los parámetros son view, keyCode y event. El cuerpo de la función consiste en handleKeyEvent(view, keyCode), que utiliza los parámetros pasados y muestra un Boolean.

f73fe767b8950123.png

5. Cómo crear listas de palabras

Ahora analicemos todo lo que aprendiste sobre las colecciones, las lambdas y las funciones de orden superior, y apliquémoslo a un caso de uso realista.

Supongamos que quieres crear una app de Android para jugar un juego de palabras o aprender palabras de vocabulario. La app podría tener el siguiente aspecto, con un botón para cada letra del alfabeto:

7539df92789fad47.png

Si haces clic en la letra A, aparecerá una lista breve de algunas palabras que comienzan con la letra A, y así sucesivamente.

Necesitarás una colección de palabras, pero ¿qué tipo de colección? Si la app incluirá algunas palabras que comienzan con cada letra del alfabeto, necesitarás una forma de encontrar u organizar todas las palabras que comiencen con una letra determinada. Para dificultar el desafío, te convendría elegir diferentes palabras de la colección cada vez que el usuario ejecute la app.

Primero, empieza con una lista de palabras. Si quieres una app real, debes tener una lista más larga de palabras y agregar palabras que comiencen con todas las letras del alfabeto, pero con una lista corta basta para trabajar por ahora.

  1. Reemplaza el código en el Playground de Kotlin con este código:
fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
}
  1. Para obtener una colección de las palabras que comienzan con la letra B, puedes usar filter con una expresión lambda. Agrega estas líneas:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
println(filteredWords)

La función startsWith() muestra el valor "true" si una string comienza con una string especificada. También puedes indicarle que ignore las mayúsculas y minúsculas a fin de que "b" coincida con "b" o "B".

  1. Ejecuta la app y observa el resultado:
[balloon, best, brief]
  1. Recuerda que quieres usar las palabras al azar para tu app. Con las colecciones de Kotlin, puedes usar la función shuffled() para hacer una copia de una colección con los elementos mezclados. También cambia las palabras filtradas para que se mezclen:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
  1. Ejecuta el programa y observa los nuevos resultados:
[brief, balloon, best]

Como las palabras están mezcladas al azar, es posible que las veas en un orden diferente.

  1. No necesitas todas las palabras (especialmente si tu lista real es larga), solo unas pocas. Puedes usar la función take() para obtener los primeros elementos de la colección. Haz que las palabras filtradas solo incluyan las dos primeras palabras mezcladas:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
  1. Ejecuta el programa y observa los nuevos resultados:
[brief, balloon]

Debido a la mezcla al azar, es posible que veas palabras diferentes cada vez que lo ejecutes.

  1. Por último, para la app, quieres la lista aleatoria de palabras para cada letra ordenada. Como antes, puedes usar la función sorted() para mostrar una copia de la colección con los elementos ordenados:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
    .sorted()
  1. Ejecuta el programa y observa los nuevos resultados:
[balloon, brief]

Todo el código anterior:

fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
    val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
        .shuffled()
        .take(2)
        .sorted()
    println(filteredWords)
}
  1. Intenta cambiar el código para crear una lista de una palabra aleatoria que comience con la letra C. ¿Qué debe cambiar en el código anterior?
val filteredWords = words.filter { it.startsWith("c", ignoreCase = true) }
    .shuffled()
    .take(1)

En la app real, deberás aplicar el filtro para cada letra del alfabeto, pero ahora sabes cómo generar la lista de palabras para cada letra.

Las colecciones son potentes y flexibles. Hay muchas acciones que pueden realizar, y puede haber más de una forma de realizarlas. A medida que aprendas más sobre programación, empezarás a determinar qué tipo de recopilación es adecuada para el problema en cuestión y cuáles son las mejores formas de procesarlo.

Las lambdas y las funciones de orden superior hacen que trabajar con colecciones sea más fácil y conciso. Estas ideas son muy útiles, por lo que las verás en acción una y otra vez.

6. Resumen

  • Una colección es un grupo de elementos relacionados.
  • Las colecciones pueden ser mutables o inmutables.
  • Las colecciones se pueden estar ordenadas o desordenadas.
  • Las colecciones pueden requerir elementos únicos o permitir duplicados.
  • Kotlin admite diferentes tipos de colecciones, incluidos listas, conjuntos y mapas.
  • Kotlin ofrece muchas funciones para procesar y transformar colecciones, entre ellas, forEach, map, filter y sorted.
  • Una lambda es una función sin nombre que se puede pasar como una expresión de inmediato. Por ejemplo, { a: Int -> a * 3 }.
  • Una función de orden superior significa pasar una función a otra función o mostrar una función desde otra.

7. Más información