Lección 3 de 8. Hasta ahora los programas iban en línea recta. Aquí aprendes a decidir con if y when, que en Kotlin además devuelven valores, a repetir con rangos y bucles, y a guardar muchos datos en listas, mapas y conjuntos. Verás también la idea que distingue a las colecciones de Kotlin: las de solo lectura y las mutables son tipos distintos, y el compilador no deja confundirlas.
if también devuelve un valor
El if de Kotlin se escribe como en Java, C o JavaScript: una condición entre paréntesis y un bloque entre llaves. La condición tiene que ser un Boolean; un número o un texto no cuentan como «verdadero».
fun main() {
val temperatura = 31
if (temperatura >= 30) {
println("Hace calor")
} else if (temperatura >= 15) {
println("Se está bien")
} else {
println("Hace frío")
}
}salidaHace calor
La novedad es que en Kotlin if es una expresión: devuelve un valor, el de la rama que se ejecutó. Por eso Kotlin no tiene el operador ternario condición ? a : b de Java: no le hace falta.
fun main() {
val temperatura = 12
val ropa = if (temperatura >= 20) "playera" else "suéter"
println("Hoy toca $ropa")
val mensaje = if (temperatura < 0) {
println("(alerta de helada)")
"Quédate en casa"
} else {
"Puedes salir"
}
println(mensaje)
}salidaHoy toca suéter Puedes salir
Si una rama es un bloque, el valor es la última expresión del bloque. Cuando se usa como expresión, el else es obligatorio: sin él, no habría valor que dar en el otro caso.
when: el switch que devuelve valores
when compara un valor con varias opciones y ejecuta la primera que coincida. Cada rama puede tener varios valores separados por comas, un rango con in o una comprobación de tipo con is:
fun describir(x: Any): String = when (x) {
0 -> "cero"
1, 2, 3 -> "poquito"
in 4..9 -> "un dígito"
is String -> "un texto de ${x.length} letras"
else -> "otra cosa"
}
fun main() {
println(describir(0))
println(describir(2))
println(describir(7))
println(describir("Mazatlán"))
println(describir(3.5))
}salidacero poquito un dígito un texto de 8 letras otra cosa
Any es el tipo del que descienden todos, así que describir acepta cualquier valor. Fíjate en la rama is String: dentro de ella, Kotlin ya sabe que x es un texto y deja usar x.length sin conversiones. Es un smart cast, y lo verás mucho en la lección 4.
when también funciona sin valor entre paréntesis: entonces cada rama es una condición, y es la forma clara de escribir una cadena de if/else if:
fun tarifa(edad: Int, estudiante: Boolean): Int = when {
edad < 3 -> 0
edad >= 60 -> 40
estudiante -> 50
else -> 90
}
fun main() {
println(tarifa(2, false))
println(tarifa(65, false))
println(tarifa(20, true))
println(tarifa(35, false))
}salida0 40 50 90
El error que te ahorra: un when al que le falta un caso
fun nombreDelDia(n: Int): String = when (n) {
1 -> "lunes"
2 -> "martes"
3 -> "miércoles"
}
fun main() {
println(nombreDelDia(4))
}kotlincwhen_sin_else.kt:1:36: error: 'when' expression must be exhaustive. Add an 'else' branch.
fun nombreDelDia(n: Int): String = when (n) {
^^^^Un when que devuelve un valor tiene que ser exhaustivo: cubrir todos los casos posibles. Con un Int eso solo se logra con un else. Con un enum o una clase sealed (lección 5) basta con nombrar todas las opciones, y si mañana alguien añade una nueva, el compilador te señala cada when que se quedó corto.
Si vienes de Java: when frente a switch
El switch clásico de Java necesita un break en cada caso y, si se te olvida, la ejecución «se cae» al siguiente. when nunca se cae: cada rama es independiente. Las switch expressions de Java 14 en adelante (case 1 -> ...) se parecen mucho a when; lo tienes comparado en las versiones de Java.
Rangos
Un rango es una secuencia de valores entre dos extremos. Se usan en bucles, en when y para preguntar si un valor está dentro con in:
fun main() {
println((1..5).toList())
println((1..<5).toList())
println((1 until 5).toList())
println((5 downTo 1).toList())
println((1..9 step 3).toList())
println(('a'..'e').joinToString(""))
val calificacion = 87
println(calificacion in 80..89)
println(calificacion !in 0..100)
}salida[1, 2, 3, 4, 5] [1, 2, 3, 4] [1, 2, 3, 4] [5, 4, 3, 2, 1] [1, 4, 7] abcde true false
..< y until son lo mismo; ..< es más reciente y se lee mejor. toList() está ahí solo para poder imprimir los valores: el rango en sí no guarda los números, solo sus extremos y el paso, así que 1..1_000_000 no ocupa memoria.
Bucles: for y while
El for de Kotlin siempre recorre algo: un rango, una lista, un texto, un mapa. No existe el for (int i = 0; i < n; i++) de Java; su equivalente es un rango.
fun main() {
for (i in 1..3) {
println("Vuelta $i")
}
val frutas = listOf("mango", "guayaba", "tuna")
for (fruta in frutas) {
print("$fruta ")
}
println()
for ((i, fruta) in frutas.withIndex()) {
println("$i: $fruta")
}
for (i in frutas.indices.reversed()) {
print("${frutas[i]} ")
}
println()
}salidaVuelta 1 Vuelta 2 Vuelta 3 mango guayaba tuna 0: mango 1: guayaba 2: tuna tuna guayaba mango
withIndex() da a la vez la posición y el valor, y los paréntesis de (i, fruta) los separan en dos variables: eso se llama desestructurar. indices es el rango de posiciones válidas de la lista, de 0 a size - 1.
while repite mientras se cumpla la condición; do…while la comprueba al final, así que se ejecuta al menos una vez. break sale del bucle y continue salta a la siguiente vuelta:
fun main() {
var saldo = 1000
var meses = 0
while (saldo > 0) {
saldo -= 300
meses++
}
println("Se acabó en $meses meses; quedó $saldo")
for (n in 1..10) {
if (n % 2 == 0) continue
if (n > 7) break
print("$n ")
}
println()
}salidaSe acabó en 4 meses; quedó -200 1 3 5 7
Listas
Kotlin separa las colecciones en dos familias: las de solo lectura, que se crean con listOf, setOf y mapOf, y las mutables, con mutableListOf, mutableSetOf y mutableMapOf. La diferencia está en el tipo: una List no tiene métodos para cambiarla.
fun main() {
val estados = listOf("Oaxaca", "Jalisco", "Yucatán", "Sonora")
println(estados.size)
println(estados[0])
println(estados.first() + " y " + estados.last())
println("Oaxaca" in estados)
println(estados.indexOf("Sonora"))
println(estados.getOrNull(10))
println(estados.sorted())
println(estados.joinToString(" / "))
}salida4 Oaxaca Oaxaca y Sonora true 3 null [Jalisco, Oaxaca, Sonora, Yucatán] Oaxaca / Jalisco / Yucatán / Sonora
Los corchetes dan el elemento por su posición, desde 0. getOrNull devuelve null si la posición no existe, en vez de fallar. Y sorted() no ordena la lista: devuelve otra lista ordenada, porque la original es de solo lectura. Si intentas añadirle algo, el compilador no encuentra el método:
fun main() {
val estados = listOf("Jalisco", "Oaxaca")
estados.add("Sonora")
}kotlinclista_add.kt:3:13: error: unresolved reference 'add' on receiver of type 'List<String>'.
estados.add("Sonora")
^^^«Unresolved reference» significa que add no existe para ese tipo. Para una lista que crece, usa mutableListOf; si empieza vacía, hay que decir de qué tipo serán sus elementos:
fun main() {
val pendientes = mutableListOf<String>()
pendientes.add("pagar la luz")
pendientes.add("comprar tortillas")
pendientes += "llamar a mamá"
pendientes.removeAt(0)
pendientes[0] = "comprar tortillas y queso"
println(pendientes)
println(pendientes.isEmpty())
}salida[comprar tortillas y queso, llamar a mamá] false
Pedir una posición que no existe con los corchetes sí es un error, pero al ejecutar:
fun main() {
val numeros = listOf(10, 20, 30)
println(numeros[3])
}salida con errorException in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
at Fuera_de_rangoKt.main(fuera_de_rango.kt:3)La excepción habla de un array porque la lista que crea listOf se guarda por dentro en uno. Si no sabes si la posición existe, pregunta antes con i in lista.indices o usa getOrNull.
Si vienes de Java: List no es lo mismo en los dos
Por dentro, una List de Kotlin es una java.util.List normal: puedes pasarla a cualquier método de Java. La diferencia está en el compilador de Kotlin, que no te deja llamar a add o remove sobre una List. En Java, List.of(...) sí deja escribir add y la lista se queja al ejecutar con UnsupportedOperationException. Ojo: «solo lectura» no es lo mismo que «inmutable»; lo explicamos en las rarezas de Kotlin.
Mapas
Un mapa guarda pares de clave y valor. Cada par se escribe con to:
fun main() {
val ladas = mapOf("CDMX" to "55", "Guadalajara" to "33", "Monterrey" to "81")
println(ladas["Monterrey"])
println(ladas["Tijuana"])
println(ladas.getOrDefault("Tijuana", "sin dato"))
println(ladas.keys)
println(ladas.size)
for ((ciudad, lada) in ladas) {
println("$ciudad: $lada")
}
}salida81 null sin dato [CDMX, Guadalajara, Monterrey] 3 CDMX: 55 Guadalajara: 33 Monterrey: 81
Buscar con corchetes una clave que no existe no falla: devuelve null. Por eso el tipo de ladas["Monterrey"] es String? y no String, y el compilador te obliga a contemplar que falte. Si estás seguro de que la clave existe, getValue devuelve el valor directamente y lanza una excepción si no está. Un mapa conserva el orden en que se insertaron las claves.
Un uso clásico de un mapa mutable es contar cosas:
fun main() {
val texto = "el perro y el gato y el loro"
val cuenta = mutableMapOf<String, Int>()
for (palabra in texto.split(" ")) {
cuenta[palabra] = cuenta.getOrDefault(palabra, 0) + 1
}
println(cuenta)
println(cuenta.maxBy { it.value }.key)
}salida{el=3, perro=1, y=2, gato=1, loro=1}
elLa última línea usa una lambda, el bloque entre llaves, para decir por qué valor se busca el máximo. Las lambdas son el tema de la lección 6; ahí verás que este conteo cabe en una línea con groupingBy.
Conjuntos
Un Set no admite repetidos y responde muy rápido a la pregunta «¿está este elemento?»:
fun main() {
val vistos = mutableSetOf<String>()
for (correo in listOf("ana@example.com", "luis@example.com", "ana@example.com")) {
if (!vistos.add(correo)) {
println("Repetido: $correo")
}
}
println(vistos)
val lunes = setOf("Ana", "Luis", "Eva")
val martes = setOf("Luis", "Eva", "Raúl")
println(lunes intersect martes)
println(lunes union martes)
println(lunes subtract martes)
}salidaRepetido: ana@example.com [ana@example.com, luis@example.com] [Luis, Eva] [Ana, Luis, Eva, Raúl] [Ana]
add devuelve false si el elemento ya estaba, lo que permite detectar repetidos sin buscar antes. intersect, union y subtract se pueden escribir entre los dos conjuntos, sin punto ni paréntesis, porque son funciones infix.
Arrays
También existen los arrays de tamaño fijo, arrayOf(...) y, para números, IntArray o intArrayOf(...), que por dentro son el int[] de Java. En el código de Kotlin se usan poco, casi siempre para hablar con bibliotecas de Java o cuando el rendimiento importa. Una advertencia: si imprimes un array directamente, sale algo como [I@1b6d3586 (el tipo y una dirección); para ver su contenido usa contentToString():
fun main() {
val dias = IntArray(7)
dias[0] = 5
dias[6] = 2
println(dias.contentToString())
println(dias.sum())
val colores = arrayOf("rojo", "verde")
println(colores.contentToString())
}salida[5, 0, 0, 0, 0, 0, 2] 7 [rojo, verde]
Resumen
- ParaEscribeRecuerda
- Elegir un valor
val x = if (c) a else bno hay operador ternario - Varias opciones
when (x) { 1, 2 -> ... ; in 3..9 -> ... ; else -> ... }como expresión, exhaustivo - Contar de 1 a n
for (i in 1..n)..<excluye el último - Hacia atrás o con paso
10 downTo 1 step 2los rangos no ocupan memoria - Posición y valor
for ((i, x) in lista.withIndex())desestructurar - Una lista fija
listOf(...)sinadd;sorted()da otra - Una lista que crece
mutableListOf<String>()+=añade - Clave y valor
mapOf("a" to 1)m[k]puede sernull - Sin repetidos
setOf(...)adddevuelve si era nuevo
Retos
Reto 1. Escribe el clásico FizzBuzz del 1 al 15 con when: si el número es múltiplo de 3 y de 5, escribe «FizzBuzz»; si solo de 3, «Fizz»; si solo de 5, «Buzz»; si no, el número.
Ver la solución
fun main() {
for (n in 1..15) {
val texto = when {
n % 15 == 0 -> "FizzBuzz"
n % 3 == 0 -> "Fizz"
n % 5 == 0 -> "Buzz"
else -> n.toString()
}
print("$texto ")
}
println()
}salida1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
El orden de las ramas importa: when se queda con la primera que se cumple, así que el caso de 15 va antes que los de 3 y 5. Y como es una expresión, todas las ramas dan un String: por eso el número se convierte con toString().
Reto 2. Cuenta cuántas veces aparece cada vocal en «murcielago» y escríbelo como un mapa. Las vocales que no aparezcan no deben salir.
Ver la solución
fun main() {
val vocales = mutableMapOf<Char, Int>()
for (letra in "murcielago") {
if (letra in "aeiou") {
vocales[letra] = vocales.getOrDefault(letra, 0) + 1
}
}
println(vocales)
}salida{u=1, i=1, e=1, a=1, o=1}Un String se recorre con for letra a letra, y letra in "aeiou" pregunta si ese carácter está en el texto. Las claves salen en el orden en que aparecieron por primera vez.
Reto 3. Suma los números pares del 1 al 100 sin usar if.
Ver la solución
fun main() {
var suma = 0
for (n in 2..100 step 2) {
suma += n
}
println(suma)
println((2..100 step 2).sum())
}salida2550 2550
Con un rango que empieza en 2 y avanza de 2 en 2 no hace falta preguntar si cada número es par. La segunda línea hace lo mismo sin bucle: un rango ya sabe sumarse.
Todo el código de esta lección se compiló con Kotlin 2.4.21 (kotlinc) y se ejecutó en la JVM de Temurin 25.0.5 en un contenedor limpio y sin red antes de publicarla; los mensajes del compilador y las salidas están copiados de esa ejecución. ¿Algo no te compila? Revisa tu versión con kotlinc -version.
