Clases y objetos en Kotlin: Modela tus datos con elegancia

Introducción

Si has llegado hasta aquí, ya sabes cómo manejar variables, colecciones, controlar el flujo de tu programa y crear funciones o lambdas para organizar tus acciones. ¡Enhorabuena! Tienes unas bases de programación fabulosas. Pero ahora toca dar el siguiente paso lógico y entrar de lleno en la Programación Orientada a Objetos (POO).

Imagina que en tu aplicación tienes que gestionar la información de decenas de usuarios: su nombre, su identificador, su correo y su edad. ¿Vas a usar variables sueltas por todo el código para cada cosita de cada usuario? Sería un absoluto caos y muy propenso a errores. Para eso están las clases y los objetos. En esta unidad, vamos a ver cómo Kotlin hace que crear y agrupar estructuras de datos sea un proceso muy limpio, y descubriremos la magia de las data classes (clases de datos), una de las características estrella del lenguaje que te ahorrará horas de escribir código.

¿Qué son las clases y cómo se declaran?

En Kotlin, la programación orientada a objetos es muy directa. Una clase te permite declarar un conjunto de características compartidas para un concepto. Piensa en la clase como si fuera el «molde» (por ejemplo, el concepto de Cliente) y en los objetos como las «galletas» reales que salen de ese molde (el cliente Juan, la clienta María).

Para declarar una clase, simplemente utilizamos la palabra reservada class:

class Cliente

Propiedades de una clase

Las características concretas de ese molde se llaman propiedades. Puedes declarar las propiedades de una clase directamente entre paréntesis () justo después del nombre de la misma. A esta parte se le conoce como el encabezado de la clase (class header).

class Contacto(val id: Int, var email: String)

También puedes definir propiedades adicionales dentro del cuerpo de la clase, abriendo unas llaves {}:

class Contacto(val id: Int, var email: String) {
    val categoria: String = "trabajo"
}

La regla de oro: Te recomendamos encarecidamente que declares tus propiedades como de solo lectura (val) siempre que sea posible. Usa variables mutables (var) únicamente si tienes la absoluta certeza de que ese dato necesitará cambiar después de haber creado el objeto.

Al igual que ocurría con las funciones, las propiedades de una clase pueden tener valores por defecto:

class Contacto(val id: Int, var email: String = "[email protected]") {
    val categoria: String = "trabajo"
}

El dato: Si pones parámetros en el encabezado sin escribir val o var delante, Kotlin los tratará como simples parámetros de configuración y no como propiedades reales de la clase; por lo tanto, no podrás acceder a ellos después. ¡Acuérdate de poner siempre val o var!

Nota extra: Kotlin te permite poner una coma al final de la última propiedad (conocido como trailing comma) para que te sea más fácil reordenar el código copiando y pegando líneas.

Crear instancias (objetos)

Para poder usar esa clase en la vida real, necesitas instanciarla (crear un objeto) a través de un constructor. En lenguajes más antiguos como Java tendrías que usar la palabra new a la fuerza, pero a Kotlin le gusta el código conciso, así que no hace falta.

Por defecto, el lenguaje te crea un constructor automáticamente exigiendo los parámetros que hayas puesto en el encabezado.

class Contacto(val id: Int, var email: String)

fun main() {
    // Creamos la instancia y la guardamos en una variable
    val contacto1 = Contacto(1, "[email protected]")
}

En este ejemplo:

  • Contacto es la clase (el molde).
  • contacto1 es la instancia (el objeto real en la memoria).

Acceder a las propiedades

Para leer o modificar una propiedad de un objeto que ya has creado, la sintaxis es facilísima: solo tienes que escribir el nombre de la instancia, seguido de un punto ., y el nombre de la propiedad.

fun main() {
    val contacto = Contacto(1, "[email protected]")
    
    // Imprimimos el valor de la propiedad
    println(contacto.email) 
    // Salida: [email protected]

    // Actualizamos el valor (es posible porque lo definimos como 'var')
    contacto.email = "[email protected]"
    
    // Imprimimos el nuevo valor
    println(contacto.email) 
    // Salida: [email protected]
}

Si necesitas concatenar el valor de esa propiedad dentro de un texto, recuerda usar las plantillas de cadenas con el símbolo del dólar. Como estás accediendo a una propiedad y no a una variable simple, es obligatorio envolverlo todo entre llaves ${}:

println("Su correo electrónico de empresa es: ${contacto.email}")

Funciones miembro

Las clases no son simples «bolsas» para guardar datos estáticos. También pueden tener comportamiento propio. A las funciones que declaramos dentro del cuerpo de una clase se las denomina funciones miembro (o métodos).

Para llamar a estas funciones, volvemos a usar la sintaxis del punto .:

class Contacto(val id: Int, var email: String) {
    // Esta es una función miembro
    fun imprimirId() {
        println("El identificador secreto es: $id")
    }
}

fun main() {
    val contacto = Contacto(1, "[email protected]")
    // Llamamos a la función
    contacto.imprimirId() 
    // Salida: El identificador secreto es: 1
}

El superpoder de Kotlin: data classes

Aquí es donde Kotlin brilla y demuestra por qué es un lenguaje tan popular. En el día a día, muchas veces vas a crear clases cuyo único propósito sea almacenar información de forma pasiva. Para estos casos concretos, Kotlin inventó las data classes (clases de datos).

Tienen exactamente la misma funcionalidad que las clases normales que acabamos de ver, pero el compilador de Kotlin les inyecta automáticamente por detrás un montón de funciones extra para que no tengas que escribir código repetitivo o boilerplate.

Para declararlas, solo tienes que poner la palabra data antes del class:

data class Usuario(val nombre: String, val id: Int)

Al hacer esto, ganas acceso automático a tres funciones vitales que analizamos en los siguientes puntos.

Imprimir como texto (toString)

Si imprimes una clase normal de Kotlin por la consola, te escupirá un nombre incomprensible con su referencia en memoria (algo tipo Usuario@2f92e0f4). Pero las data classes sobrescriben la función toString() de forma nativa para que la salida sea humana y legible.

fun main() {
    val usuario = Usuario("Alex", 1)
    
    println(usuario) 
    // Salida automática y bonita: Usuario(nombre=Alex, id=1)
}

Esto es indispensable cuando estás haciendo pruebas o guardando logs en tu servidor.

Comparar instancias (==)

Si tú comparas dos instancias distintas de una clase normal usando ==, Kotlin te dirá que son diferentes false, porque mirará si ocupan el mismo espacio físico en la memoria del ordenador.

Sin embargo, en las data classes, el operador == (que llama a la función equals()) es inteligente: compara los datos reales.

fun main() {
    val user1 = Usuario("Alex", 1)
    val user2 = Usuario("Alex", 1)
    val user3 = Usuario("Max", 2)

    println("user1 y user2: ${user1 == user2}") // true (¡Tienen los mismos datos!)
    println("user1 y user3: ${user1 == user3}") // false
}

Copiar instancias (copy)

Imagina que quieres crear un objeto nuevo partiendo de uno existente, pero alterando únicamente uno de sus datos. Modificar el objeto original usando un var puede causar fallos encadenados si otra parte de tu código estaba usando a ese mismo usuario.

La función copy() te clona el objeto a la perfección, y como parámetros opcionales le puedes pasar el nuevo valor para las propiedades que quieras alterar:

fun main() {
    val usuario = Usuario("Alex", 1)

    // Clonación exacta
    println(usuario.copy()) 
    // Usuario(nombre=Alex, id=1)

    // Clonación con el nombre alterado
    println(usuario.copy(nombre = "Max")) 
    // Usuario(nombre=Max, id=1)
}

Ejercicios

Aquí tienes varios ejercicios de menos a más dificultad (los tres primeros son traducciones de la documentación oficial de Kotlin y los dos últimos son extras para asentar los conceptos). Abre tu Kotlin Playground y vamos a mancharnos las manos.

El empleado

Define una data class llamada Employee con dos propiedades: name (para el nombre en formato texto) y salary (para el salario en enteros). Asegúrate de que la propiedad del salario sea mutable, ¡o de lo contrario el empleado jamás podrá recibir un aumento a final de año!

El main a continuación te muestra cómo lo vamos a usar.

// Escribe tu código aquí

fun main() {
    val emp = Employee("Mary", 20)
    println(emp)
    emp.salary += 10
    println(emp)
}

Estructuras anidadas

A veces los objetos contienen a otros objetos. Declara las data classes adicionales que se necesitan para que el siguiente código compile sin errores. Fíjate bien en la llamada dentro de main() para adivinar qué propiedades y de qué tipo necesita cada clase.

data class Person(val name: Name, val address: Address, val ownsAPet: Boolean = true)
// Escribe tu código aquí:
// data class Name(...)

fun main() {
    val person = Person(
        Name("John", "Smith"),
        Address("123 Fake Street", City("Springfield", "US")),
        ownsAPet = false
    )
    println(person)
}

Generador aleatorio de empleados

Para probar tu código en el futuro, vas a necesitar un generador que cree empleados al azar. Define una clase normal RandomEmployeeGenerator.

El constructor de la clase debe recibir el minSalary y el maxSalary (ambos enteros y mutables).

Dentro del cuerpo de la clase, guarda una lista fija de nombres potenciales. Y, por último, crea una función miembro llamada generateEmployee() que devuelva una nueva instancia de tu data class Employee usando un nombre aleatorio de tu lista y un salario aleatorio dentro de los márgenes dados.

Pista: Las listas de Kotlin tienen una extensión muy útil llamada .random() que te devuelve un ítem al azar. Usa Random.nextInt(from = minSalary, until = maxSalary) para generar el número.

import kotlin.random.Random

data class Employee(val name: String, var salary: Int)

// Escribe tu clase RandomEmployeeGenerator aquí

fun main() {
    val empGen = RandomEmployeeGenerator(10, 30)
    println(empGen.generateEmployee())
    println(empGen.generateEmployee())
    
    empGen.minSalary = 50
    empGen.maxSalary = 100
    println(empGen.generateEmployee())
}

El inventario de la tienda

Crea una clase (¡normal, no data class!) llamada Producto que reciba en su constructor un nombre de tipo String y un precio de tipo Double (ambos inmutables).

Añade una propiedad dentro del cuerpo de la clase llamada stock de tipo entero, inicializada en 0, que por supuesto sea mutable.

Crea una función miembro llamada añadirStock(cantidad: Int) que le sume esa cantidad al stock actual.

En tu función main, crea un producto, añádele 50 unidades de stock, e imprime un texto final diciendo: «Tenemos [stock] unidades de [nombre]».

Clonando clientes

Tienes una data class Cliente(val id: Int, val nombre: String, val email: String). Fíjate en que todo está blindado y es inmutable (val).

Crea una instancia para «Ana» con ID 1 y correo «[email protected]». De repente, Ana te avisa de que ha cambiado su correo corporativo a «[email protected]».

Como no puedes hacer ana.email = ... porque daría error, usa la función de las data classes que hemos aprendido para crear un nuevo objeto llamado anaActualizada basado en Ana, pero con su correo modificado. Imprime por consola a anaActualizada.

Soluciones a los ejercicios

No hagas trampas. Échale un vistazo a las soluciones únicamente cuando te hayas peleado un buen rato con tu editor de código y el compilador te haya gritado un par de veces.

El empleado

data class Employee(val name: String, var salary: Int)

fun main() {
    val emp = Employee("Mary", 20)
    println(emp)
    emp.salary += 10
    println(emp)
}

Estructuras anidadas

data class Person(val name: Name, val address: Address, val ownsAPet: Boolean = true)
data class Name(val first: String, val last: String)
data class Address(val street: String, val city: City)
data class City(val name: String, val countryCode: String)

fun main() {
    val person = Person(
        Name("John", "Smith"),
        Address("123 Fake Street", City("Springfield", "US")),
        ownsAPet = false
    )
    println(person)
}

Generador aleatorio de empleados

import kotlin.random.Random

data class Employee(val name: String, var salary: Int)

class RandomEmployeeGenerator(var minSalary: Int, var maxSalary: Int) {
    // Definimos la lista en el cuerpo de la clase
    val names = listOf("John", "Mary", "Ann", "Paul", "Jack", "Elizabeth")
    
    // Función miembro que devuelve un Employee
    fun generateEmployee() = Employee(
        names.random(), 
        Random.nextInt(from = minSalary, until = maxSalary)
    )
}

fun main() {
    val empGen = RandomEmployeeGenerator(10, 30)
    println(empGen.generateEmployee())
    println(empGen.generateEmployee())
    
    empGen.minSalary = 50
    empGen.maxSalary = 100
    println(empGen.generateEmployee())
}

El inventario de la tienda

class Producto(val nombre: String, val precio: Double) {
    // Propiedad en el cuerpo inicializada a 0
    var stock: Int = 0
    
    fun añadirStock(cantidad: Int) {
        stock += cantidad
    }
}

fun main() {
    val miProducto = Producto("Teclado Mecánico", 45.99)
    miProducto.añadirStock(50)
    
    println("Tenemos ${miProducto.stock} unidades de ${miProducto.nombre}")
}

Clonando clientes

data class Cliente(val id: Int, val nombre: String, val email: String)

fun main() {
    // Creamos nuestra instancia original
    val ana = Cliente(1, "Ana", "[email protected]")
    
    // Clonamos el objeto modificando únicamente la propiedad email
    val anaActualizada = ana.copy(email = "[email protected]")
    
    // Comprobamos la magia
    println(anaActualizada)
    // Salida: Cliente(id=1, nombre=Ana, [email protected])
}

Control de flujo en Kotlin: Condicionales (if, when) y bucles (for, while)

Introducción

Hasta ahora, todos los programas que hemos escrito se ejecutaban en línea recta: el ordenador leía la línea 1, luego la 2, luego la 3… y terminaba. Pero en el mundo real, las aplicaciones necesitan tomar decisiones (si el usuario tiene saldo, haz la compra; si no, muestra un error) y repetir tareas (mostrar los 50 mensajes de un chat uno por uno).

Para esto sirve el Control de Flujo. En Kotlin, contamos con herramientas modernizadas y súper potentes para dirigir el tráfico de nuestro código. ¡Vamos a descubrirlas!

Tomando decisiones: el clásico if / else

La forma más básica de tomar una decisión en programación es usar la estructura if (si ocurre esto…) y else (si no, haz esto otro…).

La condición a evaluar siempre va entre paréntesis (), y el bloque de código que se ejecutará va entre llaves {}.

fun main() {
    val edad = 18

    if (edad >= 18) {
        println("Puedes entrar a la discoteca.")
    } else {
        println("Lo siento, vuelve a casa.")
    }
}

El súper-poder de Kotlin: el if como expresión

Si vienes de lenguajes como Java o JavaScript, conocerás el famoso operador ternario (condicion ? valor1 : valor2) para asignar variables en una sola línea. En Kotlin, el operador ternario no existe porque no hace falta.

En Kotlin, un if puede devolver un valor directamente. Si tu if y tu else solo tienen una línea, puedes quitar las llaves y hacerlo así de elegante:

fun main() { 
    val a = 10
    val b = 20

    // El resultado del if se guarda directamente en la variable 'mayor'
    val mayor = if (a > b) a else b 
    
    println("El número mayor es $mayor") // Imprime: 20
}

El condicional when: el switch con esteroides

Cuando tienes que evaluar muchísimas opciones distintas, usar decenas de if / else if / else encadenados hace que el código sea ilegible.

Otros lenguajes usan la palabra switch. Kotlin usa when (cuando), y es una de las herramientas más queridas por los desarrolladores.

when como instrucción

Colocamos la variable que queremos evaluar entre paréntesis. Luego usamos una «flechita» -> para indicar qué hacer en cada caso. El else actúa como la opción por defecto si no se cumple ninguna de las anteriores.

fun main() {
    val boton = "X"

    when (boton) {
        "A" -> println("Saltar")
        "B" -> println("Atacar")
        "X" -> println("Abrir Inventario")
        "Y" -> println("Magia")
        else -> println("Botón no reconocido") 
    }
}

Nota: Kotlin evalúa de arriba a abajo. En cuanto encuentra una coincidencia, ejecuta esa línea y sale del when automáticamente. (¡Adiós a la pesadilla de olvidar poner los break de otros lenguajes!)

when como expresión (devolviendo un valor)

Al igual que el if, podemos usar when para asignar un valor directamente a una variable:

val estadoSemaforo = "Rojo" 
 
val accion = when (estadoSemaforo) {
    "Verde" -> "Acelerar"
    "Ambar" -> "Frenar poco a poco"
    "Rojo" -> "Detenerse"
    else -> "Llamar al mecánico" // Al devolver valor, el 'else' es OBLIGATORIO
}
println("Debes: $accion")

Rangos: preparando el terreno para los bucles

Antes de aprender a repetir tareas, necesitamos saber cómo crear Rangos (intervalos de valores) en Kotlin. Es facilísimo:

  • .. (Punto punto): Crea un rango que incluye el último número. 1..4 equivale a 1, 2, 3, 4.
  • ..< (Punto punto menor): Crea un rango que excluye el último número. 1..<4 equivale a 1, 2, 3.
  • downTo: Cuenta hacia atrás. 4 downTo 1 equivale a 4, 3, 2, 1.
  • step: Cambia el tamaño del salto. 1..5 step 2 equivale a 1, 3, 5.

(¡También funciona con letras del abecedario! Ej: 'a'..'d')

Bucles: repitiendo tareas sin cansarse

El bucle for (para cada…)

Se usa cuando sabes exactamente cuántas veces quieres repetir algo, o cuando quieres recorrer una Colección (como las Listas que vimos en el artículo anterior).

fun main() {
    // Repetir un código un número exacto de veces usando un rango
    for (numero in 1..5) { 
        print(numero) // Imprime: 12345
    }

    println() // Salto de línea

    // Recorrer una lista
    val pasteles = listOf("Zanahoria", "Queso", "Chocolate")
    for (pastel in pasteles) {
        println("¡Qué rico, un pastel de $pastel!")
    }
}

Los bucles while y do-while (mientras que…)

Se usan cuando no sabes cuántas veces se va a repetir algo, pero sabes que debe repetirse «mientras» se cumpla una condición.

  • while: Primero comprueba la condición. Si es falsa desde el principio, nunca se ejecuta.
  • do-while: Primero ejecuta el código una vez, y luego comprueba la condición. Te asegura que el bloque de código se va a ejecutar como mínimo una vez.
fun main() {
    var porcionesComidas = 0
    
    // Bucle while normal
    while (porcionesComidas < 3) {
        println("Me como una porción")
        porcionesComidas++ // Esto suma 1 a la variable (es lo mismo que porcionesComidas = porcionesComidas + 1)
    }
}

Ejercicios

Abre tu Kotlin Playground y vamos a machacar lo aprendido.

Los dados

Crea un minijuego donde ganas si al lanzar dos dados sacas el mismo número. Si son iguales, imprime "¡Has ganado :)". Si no, "Has perdido :(".

(Nota: Para generar números aleatorios usaremos una librería nativa de Kotlin llamada Random).

import kotlin.random.Random

fun main() {
    // Genera un número aleatorio entre 0 y 5.
    val dado1 = Random.nextInt(6) 
    val dado2 = Random.nextInt(6)
    
    println("Dado 1: $dado1 | Dado 2: $dado2")
    // Escribe tu código (if/else) a partir de aquí:
    
}

Botones de consola

Usando un when que actúe como expresión (asignando su resultado o metiéndolo directo en un println), haz que el programa imprima la acción correspondiente al botón pulsado:

  • A -> «Sí»
  • B -> «No»
  • X -> «Menú»
  • Y -> «Nada»
  • Cualquier otro -> «No existe ese botón»
fun main() {
    val boton = "A"
    
    // Escribe tu when aquí dentro del println
    println(
        // ...
    )
}

Comiendo Pizza

Tienes un código muy feo que cuenta porciones de pizza repitiendo líneas a mano. Conviértelo en un bucle while que cuente automáticamente hasta llegar a las 8 porciones.

fun main() {
    var porciones = 0
    // ¡Borra este desastre y usa un bucle while!
    porciones++
    println("Solo hay $porciones porción/es de pizza :(")
    porciones++
    println("Solo hay $porciones porción/es de pizza :(")
    // ... así hasta 7 ...
    
    // Al salir del bucle debe imprimir esto (y la variable debe valer 8):
    println("¡Tenemos $porciones porciones! ¡Una pizza entera! :D")
}

El clásico reto FizzBuzz

Este es uno de los ejercicios más famosos en las entrevistas de programación junior.

Escribe un programa que imprima los números del 1 al 100, pero:

  • Si el número es divisible por 3, imprime la palabra "fizz" en lugar del número.
  • Si el número es divisible por 5, imprime "buzz".
  • Si el número es divisible por 3 Y por 5 (es decir, divisible por 15), imprime "fizzbuzz".

Pista: Usa un bucle for del 1 al 100. Dentro, usa un when sin argumento para evaluar condiciones usando el operador Módulo % (que te da el resto de una división. Si numero % 3 == 0, es que es divisible por 3).

fun main() {
    // Escribe tu código aquí (for + when)
}

Soluciones a los ejercicios

No hagas trampas. Échale un vistazo a las soluciones únicamente cuando te hayas peleado un buen rato con el código.

Los dados

import kotlin.random.Random

fun main() {
    val dado1 = Random.nextInt(6)
    val dado2 = Random.nextInt(6)
    println("Dado 1: $dado1 | Dado 2: $dado2")
    
    // Comprobamos la igualdad con ==
    if (dado1 == dado2) {
        println("¡Has ganado :)")
    } else {
        println("Has perdido :(")
    }
}

Botones de consola

fun main() {
    val boton = "A"
    
    println(
        when (boton) {
            "A" -> "Sí"
            "B" -> "No"
            "X" -> "Menú"
            "Y" -> "Nada"
            else -> "No existe ese botón"
        }
    )
}

La pizza

fun main() {
    var porciones = 0
    
    while (porciones < 7) {
        porciones++
        println("Solo hay $porciones porción/es de pizza :(")
    }
    
    porciones++ // Sumamos la octava porción al salir
    println("¡Tenemos $porciones porciones! ¡Una pizza entera! :D")
}

El clásico reto FizzBuzz

fun main() {
    for (numero in 1..100) {
        println(
            // Al usar when SIN variable, podemos evaluar condiciones booleanas libres
            // ¡El orden importa! Hay que comprobar el 15 primero.
            when {
                numero % 15 == 0 -> "fizzbuzz"
                numero % 3 == 0 -> "fizz"
                numero % 5 == 0 -> "buzz"
                else -> numero // Si no cumple ninguna, imprime el número normal
            }
        )
    }
}

Guía Definitiva de colecciones en Kotlin: listas, conjuntos y mapas explicados desde cero

Introducción

En las unidades anteriores aprendimos a guardar un dato individual en una variable (como un número o un texto). Pero, ¿qué pasa si estás creando una app y necesitas guardar los nombres de 100 usuarios? ¿Vas a crear 100 variables distintas? ¡Por supuesto que no!

Para eso existen las colecciones. Son estructuras que nos permiten agrupar múltiples datos bajo un mismo nombre para procesarlos más tarde.

En Kotlin, la regla de seguridad que vimos con val y var se mantiene: las colecciones pueden ser de solo lectura (inmutables) o mutables (modificables). Kotlin nos ofrece tres tipos principales. ¡Vamos a destriparlas!

Listas (Lists): El cajón ordenado

Una Lista (List) es exactamente lo que imaginas: una colección de elementos ordenados uno detrás de otro.

  • Tienen un orden estricto (el primero, el segundo, el tercero…).
  • Permiten duplicados (puedes tener el mismo elemento varias veces).

Crear listas

Para crear una lista de solo lectura usamos listOf(). Si queremos una lista a la que podamos añadir o quitar cosas en el futuro, usamos mutableListOf().

fun main() { 
    // Lista de solo lectura (Kotlin deduce que es de tipo String)
    val formas = listOf("triángulo", "cuadrado", "círculo")
    println(formas) // [triángulo, cuadrado, círculo]
    
    // Lista mutable (Aquí le decimos explícitamente el tipo <String>)
    val formasMutables: MutableList<String> = mutableListOf("triángulo", "cuadrado", "círculo")
}

Profundizando: El índice cero y los errores comunes

Como las listas están ordenadas, cada elemento tiene una posición o «índice». ¡Ojo! En programación, siempre empezamos a contar desde el cero.

Piensa en los ascensores en España: cuando entras al edificio desde la calle, estás en la Planta Baja (índice 0). Si subes un piso, llegas a la Planta 1 (el índice 1, que en realidad es el segundo nivel). El índice te dice cuántos saltos das desde el principio.

fun main() { 
    val formas = listOf("triángulo", "cuadrado", "círculo")
    
    // Acceder por su posición (índice)
    println("El primer elemento es: ${formas[0]}") // triángulo
    
    // ¡CUIDADO! El terror de los novatos: IndexOutOfBoundsException
    // println(formas[3]) -> ¡El programa explotará porque no hay un 4º elemento!
}

Kotlin también nos regala funciones súper útiles para no tener que lidiar siempre con los números:

fun main() {
    val formas = listOf("triángulo", "cuadrado", "círculo")
    
    println("El primer elemento: ${formas.first()}") // triángulo
    println("El último elemento: ${formas.last()}") // círculo
    println("Total de elementos: ${formas.count()}") // 3
    
    // Comprobar si algo existe con la palabra 'in'
    println("círculo" in formas) // true
}

Modificar listas mutables y el «truco del candado»

Si tu lista es MutableList, usas .add() para añadir y .remove() para borrar.

val carrito: MutableList<String> = mutableListOf("Manzanas", "Pan")
carrito.add("Leche") // [Manzanas, Pan, Leche]
carrito.remove("Pan") // [Manzanas, Leche]

A veces tienes una lista mutable, pero se la vas a pasar a otra parte de tu código y no quieres que se modifique por accidente. Puedes «disfrazarla» de solo lectura:

val inventarioMutable: MutableList<String> = mutableListOf("Espada", "Poción")
val inventarioBloqueado: List<String> = inventarioMutable // ¡Candado puesto! Ahora no se puede modificar.

Conjuntos (Sets): El VIP de la exclusividad

Un conjunto (Set) es parecido a una lista, pero con dos diferencias vitales:

  • No tienen orden (no puedes pedir el elemento [0]).
  • NO permiten elementos duplicados. Son únicos y exclusivos.

Se crean con setOf() y mutableSetOf().

fun main() {
    // Fíjate que ponemos "cereza" dos veces
    val frutas = setOf("manzana", "plátano", "cereza", "cereza")
    
    // Al imprimir, ¡la segunda cereza ha desaparecido mágicamente!
    println(frutas) // [manzana, plátano, cereza]
    println("Hay ${frutas.count()} frutas únicas") // 3
}

Profundizando: ¿Por qué usar Sets? ¡Por la velocidad!

Si quieres saber si el email «[email protected]» está en una Lista de 1 millón de usuarios, el ordenador mirará uno a uno. Es lentísimo.

Si usas un Set, Kotlin usa matemáticas internas (una función Hash) para saber exactamente dónde está guardado. Lo encuentra al instante. ¡Usa Sets cuando trabajes con muchos datos únicos!

val millonesDeUsuarios = setOf("[email protected]", "[email protected]")
if ("[email protected]" in millonesDeUsuarios) {
    println("¡El usuario ya existe!") // Comprobación súper rápida
}

Mapas (Maps): El diccionario de datos

Un Mapa (Map) almacena datos en pares Clave-Valor (Key-Value).

Piénsalo como la carta de un restaurante: el nombre del plato es la clave y su precio es el valor.

  • Las claves (keys) deben ser únicas.
  • Los valores (values) sí pueden repetirse.

Se crean con mapOf() y mutableMapOf(). Usamos la palabrita to para unir la pareja.

fun main() {
    // Especificamos explícitamente: Claves String, Valores Int
    val menuZumos: MutableMap<String, Int> = mutableMapOf("manzana" to 3, "kiwi" to 4)
    
    // Leer un valor usando su clave entre corchetes
    println("El zumo de manzana cuesta: ${menuZumos["manzana"]}€") // 3€
}

Profundizando: Nulos y sobreescritura

¿Qué pasa si buscas algo que no existe en el menú? Kotlin te devuelve null (nulo, vacío). Así evita que tu aplicación se cuelgue por un error.

println(menuZumos["piña"]) // Imprime: null

¿Y si añado una clave que ya existe? Se sobreescribe el valor antiguo. Es la forma oficial de actualizar datos:

menuZumos["manzana"] = 5 // ¡Inflación! Actualizamos el precio a 5€
menuZumos["coco"] = 6    // Como "coco" no existe, lo añade nuevo al mapa
menuZumos.remove("kiwi") // Eliminamos el kiwi

println(menuZumos.keys)   // [manzana, coco]
println(menuZumos.values) // [5, 6]

Ejercicios

Abre el Kotlin Playground e intenta resolver estos ejercicios para asentar lo aprendido.

Sumando listas

Tienes una lista de números «verdes» y otra de «rojos». Imprime cuántos números hay en total sumando el tamaño de ambas listas.

fun main() {
    val numerosVerdes = listOf(1, 4, 23)
    val numerosRojos = listOf(17, 2)
    // Escribe tu código aquí:
}

El protocolo de red

Tienes un Set con protocolos en mayúsculas. Un usuario pide «smtp» en minúsculas. Comprueba si está soportado (debe devolver un Boolean).

Pista: Usa .uppercase() en la petición para pasarla a mayúsculas antes de buscar en el Set con in.

fun main() {
    val SOPORTADOS = setOf("HTTP", "HTTPS", "FTP")
    val peticion = "smtp"
    val estaSoportado = // Escribe tu código aquí 
    println("Soporte para $peticion: $estaSoportado")
}

Diccionario de números

Crea un Map que relacione los números del 1 al 3 con su nombre escrito («uno», «dos», «tres»). Imprime cómo se escribe el número 2 buscándolo en el mapa.

fun main() {
    val numeroAPalabra = // Escribe tu código aquí
    val n = 2
    // Imprime el resultado
}

El inventario del héroe

Crea una lista mutable llamada inventario con: «Espada», «Escudo», «Poción».

Añade un «Arco». Elimina la «Poción». Finalmente, imprime el inventario y cuántos objetos tiene usando .count().

Soluciones a los ejercicios

¡No mires hasta haberlo intentado!

Sumando listas

fun main() {
    val numerosVerdes = listOf(1, 4, 23)
    val numerosRojos = listOf(17, 2)
    val total = numerosVerdes.count() + numerosRojos.count()
    println("Total: $total números")
}

El protocolo de red

fun main() {
    val SOPORTADOS = setOf("HTTP", "HTTPS", "FTP")
    val peticion = "smtp"
    val estaSoportado = peticion.uppercase() in SOPORTADOS 
    println("Soporte para $peticion: $estaSoportado") // false
}

Diccionario de números

fun main() {
    val numeroAPalabra = mapOf(1 to "uno", 2 to "dos", 3 to "tres")
    val n = 2
    println("El número $n se escribe como '${numeroAPalabra[n]}'")
}

El inventario del héroe

fun main() {
    val inventario = mutableListOf("Espada", "Escudo", "Poción")
    inventario.add("Arco")
    inventario.remove("Poción")
    println("Inventario: $inventario")
    println("Tienes ${inventario.count()} objetos.")
}

Tipos básicos en Kotlin: Inferencia, números, textos y operaciones

Introducción

En la unidad anterior vimos cómo hacer nuestro primer «Hola Mundo» y cómo usar variables (val y var). Pero nos dejamos un detalle importantísimo en el tintero: ¿qué tipo de información estamos guardando exactamente en esas variables?

En Kotlin, absolutamente todo tiene un tipo. Los tipos son fundamentales porque le dicen al compilador (el «cerebro» que lee tu código) qué puedes y qué no puedes hacer con una variable. Por ejemplo, puedes multiplicar dos números, pero no puedes multiplicar dos palabras, ¿verdad?

Ahora vamos a desgranar los tipos básicos de Kotlin, a aprender a operar con ellos y a descubrir cómo Kotlin nos facilita la vida con su «magia» deductiva.

La inferencia de tipos: Kotlin es muy listo

Si recuerdas la unidad anterior, declarábamos variables así:

var clientes = 10

En ningún momento le dijimos a Kotlin: «Oye, que sepas que clientes es un número entero». Sin embargo, Kotlin lo supo al instante. Esta capacidad se llama Inferencia de Tipos (Type Inference).

Como a la variable clientes le asignamos un 10, Kotlin deduce inmediatamente que su tipo es numérico, concretamente un Int (entero). Gracias a esto, el compilador sabe que puedes realizar operaciones matemáticas con esta variable.

Mira este ejemplo de asignaciones compuestas (una forma abreviada de hacer matemáticas):

fun main() {
    var clientes = 10

    // Vienen 3 clientes más (clientes = clientes + 3)
    clientes += 3  // Ahora hay 13
    
    // Se van 5 clientes
    clientes -= 5  // Ahora hay 8
    
    // El negocio explota y multiplicamos los clientes por 2
    clientes *= 2  // Ahora hay 16
    
    // Dividimos a los clientes en 4 grupos
    clientes /= 4  // Ahora hay 4 en cada grupo

    println(clientes) 
    // Resultado: 4
}

Los tipos básicos de Kotlin: La lista completa

Aunque Kotlin infiere los tipos, a veces querrás (o necesitarás) ser explícito. Para declarar un tipo manualmente, se usan los dos puntos : después del nombre de la variable.

A continuación mostramos los tipos fundamentales.

Números enteros (sin decimales)

  • Int: Es el estándar para números sin decimales. (Ej: val año: Int = 2024)
  • Long: Se usa para números ridículamente grandes. Si un número es demasiado grande para un Int, Kotlin lo convertirá en Long automáticamente. También puedes forzarlo añadiendo una L mayúscula al final. (Ej: val estrellas: Long = 9876543210L)
  • Byte y Short: Se usan en casos muy específicos para ahorrar memoria con números pequeños. Raramente los usarás al empezar.
  • Nota: Kotlin también tiene versiones «Unsigned» (sin signo, es decir, solo positivos) como UInt o ULong marcados con una u (Ej: val puntos: UInt = 100u).

Números con decimales (coma flotante)

  • Double: Es el estándar de Kotlin para números con decimales. Tiene una precisión doble, es decir, admite muchísimos decimales. (Ej: val precio: Double = 19.99)
  • Float: Ocupa menos memoria pero es menos preciso. Para indicarle a Kotlin que quieres un Float y no un Double, debes añadir una f o F al final del número. (Ej: val temperatura: Float = 24.5f)

Booleanos (verdadero o falso)

  • Boolean: Solo puede tener dos valores: true (verdadero) o false (falso). Es la base de la lógica en programación (Ej: val estaEncendido: Boolean = true).

Caracteres y cadenas de texto

  • Char: Representa un único carácter (una sola letra, número o símbolo). Se escribe entre comillas simples ' '. (Ej: val inicial: Char = 'J')
  • String: Representa una cadena de texto (muchos caracteres juntos). Se escribe entre comillas dobles " ". (Ej: val mensaje: String = "¡Hola, mundo!")

Declarar ahora, inicializar después

A veces sabes qué tipo de dato vas a guardar, pero aún no tienes el valor exacto. Kotlin te permite declarar una variable y asignarle su valor más tarde.

Eso sí, en estos casos es obligatorio especificar el tipo explícitamente, porque Kotlin no tiene un valor inicial del que deducirlo:

fun main() {
    // Declaramos la variable especificando el tipo explícitamente, pero sin darle valor
    val d: Int 
    
    // Más adelante en el código, la inicializamos
    d = 3 

    println(d) // Imprime: 3
}

La seguridad de Kotlin frente a errores

¿Qué pasa si intentas imprimir la variable d antes de darle un valor? En otros lenguajes tu programa explotaría (el temido NullPointerException o imprimiría basura de la memoria). Kotlin no te deja hacerlo. El código directamente se pondrá en rojo y no compilará, mostrándote el error: «Variable ‘d’ must be initialized». ¡Un salvavidas enorme!

Ejercicios

Abre tu editor o el Kotlin Playground y pon a prueba lo que acabas de aprender.

El tipado explícito

Kotlin puede inferir los tipos, pero en este ejercicio queremos que seas tú quien los escriba. Modifica el siguiente código para añadir explícitamente (: Tipo) el tipo de dato correcto a cada variable:

fun main() {
    val a = 1000 
    val b = "mensaje de registro"
    val c = 3.14
    val d = 100_000_000_000_000 // Fíjate, los guiones bajos sirven para leer mejor los números grandes
    val e = false
    val f = '\n' // Esto es un carácter especial que representa un "salto de línea"
}

El cofre del tesoro

Crea un programa que simule el oro de un jugador en un videojuego usando una variable mutable (var) llamada monedas:

  1. El jugador empieza con 50 monedas.
  2. Encuentra un cofre mágico y su oro se multiplica por 3. (Usa asignación compuesta *=).
  3. Compra una espada que cuesta 80 monedas. (Usa -=).
  4. Imprime el resultado final: "Tras la aventura, te quedan X monedas".

Detecta el error

El siguiente código tiene un problema y Kotlin se quejará si intentas ejecutarlo. Corrígelo para que funcione y se imprima la edad correctamente.

fun main() {
    val edadUsuario: Int
    println("La edad del usuario es $edadUsuario")
    edadUsuario = 25
}

Cuidado con las divisiones

Si divides dos variables Int (ej: 10 / 3), Kotlin devuelve un número entero (3) y se come los decimales.

Crea dos variables llamadas dividendo (valor 10) y divisor (valor 3). Haz que sean de tipo Double explícitamente para que, al imprimirlas usando una plantilla de cadena, el resultado sea con decimales (3.3333333333333335).

Soluciones a los ejercicios

¡No mires hasta que no te hayas peleado un rato con el código!

El tipado explícito

fun main() {
    val a: Int = 1000 
    val b: String = "mensaje de registro"
    val c: Double = 3.14  // Al tener decimales y no tener 'f', es Double
    val d: Long = 100_000_000_000_000 // Es demasiado grande para ser Int
    val e: Boolean = false // Verdadero o falso
    val f: Char = '\n' // Comillas simples indican que es un solo Char
}

El cofre del tesoro

fun main() {
    var monedas = 50
    monedas *= 3
    monedas -= 80
    println("Tras la aventura, te quedan $monedas monedas") 
    // Imprimirá 70
}

Detecta el error

fun main() {
    val edadUsuario: Int
    edadUsuario = 25 // ¡Había que inicializarla ANTES de leerla!
    println("La edad del usuario es $edadUsuario")
}

Cuidado con las divisiones

fun main() {
    // Para forzar que un número entero se trate como decimal, ponemos .0
    val dividendo: Double = 10.0 
    val divisor: Double = 3.0
    
    println("El resultado exacto es ${dividendo / divisor}")
}

Primeros pasos con Kotlin: Tu primer «Hola mundo», variables y plantillas de texto

Introducción

Si estás empezando en el mundo del desarrollo (ya sea para Android o backend), Kotlin es uno de los lenguajes más modernos, limpios y seguros que puedes aprender hoy en día.

En esta unidad, vamos a dar los primeros pasos. Entenderemos cómo funciona la estructura básica de un programa, cómo almacenar información usando variables y cómo imprimir mensajes por pantalla de forma elegante. Al final, tendrás ejercicios prácticos para asentar lo aprendido.

El clásico «Hola mundo»

Por tradición, el primer programa que todo desarrollador escribe al aprender un lenguaje nuevo es el que imprime las palabras «¡Hola, mundo!» en la pantalla.

En Kotlin, se hace así de fácil:

fun main() {
    println("¡Hola, mundo!")
    // ¡Hola, mundo!
}

Analizando el código línea a línea:

  • fun: Es la palabra reservada (abreviatura de function) que usamos en Kotlin para declarar una función. Una función no es más que un bloque de código que realiza una tarea específica.
  • main(): No es una función cualquiera. Es el punto de entrada (entry point) de tu programa. Siempre que ejecutes una aplicación en Kotlin, el ordenador buscará esta función y empezará a ejecutar las instrucciones que haya dentro de ella.
  • { } (Las llaves): Todo lo que esté dentro de las llaves es el «cuerpo» de la función, es decir, las instrucciones que se van a ejecutar.
  • println(): Es una función nativa de Kotlin que coge lo que le pongas entre paréntesis (los «argumentos») y lo imprime por la salida estándar (la consola de tu pantalla). Al terminar de imprimir, añade un salto de línea (por eso termina en ln, de line). Si usas print() (sin el ln), el texto se imprimirá, pero el siguiente mensaje aparecerá pegado justo a la derecha, en la misma línea.

El dato: ¿Te has dado cuenta de algo? ¡No hay punto y coma (;) al final de la línea! En lenguajes más antiguos como Java o C++, olvidar el punto y coma era un dolor de cabeza. Kotlin es un lenguaje moderno y no los necesita.

Variables: Guardando información en memoria

Cualquier programa informático necesita almacenar datos (nombres de usuario, puntuaciones de un juego, precios…). Para eso utilizamos las variables.

En Kotlin, tenemos dos formas principales de crear variables, y esto es muy importante:

  1. Variables de solo lectura (inmutables) con val (de value).
  2. Variables mutables con var (de variable).

Para asignarle un valor a una variable, simplemente usamos el operador de asignación =. Fíjate en este ejemplo:

fun main() { 
    val palomitas = 5   // Tenemos 5 cajas de palomitas
    val perritos = 7    // Tenemos 7 perritos calientes
    var clientes = 10   // Hay 10 clientes en la cola
    
    // De repente, un par de clientes se cansan de esperar y se van de la cola
    clientes = 8
    println(clientes) 
    // Resultado en pantalla: 8
}

val vs var: ¿Cuál debería usar?

Como clientes se declaró con var, pudimos reasignarle un nuevo valor (8) más adelante en el programa. Sin embargo, si intentáramos hacer palomitas = 6, el compilador de Kotlin nos daría un error, porque palomitas es un val y no puede cambiar una vez se le ha dado un valor inicial.

La regla de oro en Kotlin: Utiliza siempre val por defecto. Usa var única y exclusivamente cuando sepas seguro que ese valor va a tener que cambiar en el futuro (como un contador, o la vida de un personaje en un juego). Esto hace que tu código sea mucho más seguro, predecible y libre de errores accidentales (los temidos bugs).

Plantillas de cadenas (String Templates): Texto inteligente

En programación, es súper común querer imprimir un texto que contenga el valor de nuestras variables. En otros lenguajes tendrías que «sumar» o concatenar trozos de texto con el símbolo +, lo cual queda feo y es fácil equivocarse.

Kotlin lo soluciona de forma magistral con las String Templates (plantillas de cadenas).

Una cadena de texto (String) se escribe siempre entre comillas dobles " ". Para inyectar el valor de una variable dentro de ese texto, solo tienes que poner el símbolo del dólar $ seguido del nombre de la variable.

Y si lo que quieres es hacer una operación matemática u otro código más complejo dentro del texto, lo metes entre llaves ${ }. Mira por ejemplo el siguiente código:

fun main() { 
    val clientes = 10
    
    // Imprimiendo una variable directamente
    println("Actualmente hay $clientes clientes esperando.")
    // Salida: Actualmente hay 10 clientes esperando.
    
    // Haciendo una operación matemática dentro del texto
    println("Si llega uno más, habrá ${clientes + 1} clientes en total.")
    // Salida: Si llega uno más, habrá 11 clientes en total.
}

La magia oculta: La inferencia de tipos

Si prestas atención, en ningún momento le hemos dicho a Kotlin que clientes es un número. Kotlin es muy listo: al ver que le asignamos un 10, él automáticamente infiere (deduce) que el tipo de dato es un número entero (Int).

Ejercicios

La programación solo se aprende tecleando. Aquí tienes varios ejercicios de menos a más dificultad. Intenta resolverlos por tu cuenta en un editor o en el Kotlin Playground antes de mirar las soluciones más abajo.

Presentando a Mary

Completa el siguiente código para que el programa imprima por consola el mensaje exacto: "Mary tiene 20 años". Debes usar obligatoriamente las variables dadas y las plantillas de cadenas ($).

fun main() {
    val nombre = "Mary"
    val edad = 20
    // Escribe tu código debajo de esta línea:
    
}

Batería del móvil

Crea un pequeño programa que simule la batería de tu móvil.

  1. Declara una variable llamada bateria que empiece al 100%. (Piensa si debe ser val o var).
  2. Imprime el texto: "La batería inicial es del 100%".
  3. Simula que juegas a un videojuego y la batería baja a 75. Actualiza el valor de la variable.
  4. Vuelve a imprimir el texto: "Tras jugar, la batería es del 75%".

Calculadora de la compra inteligente

Declara dos variables inmutables: precioZapatillas con un valor de 50, y cantidadCajas con un valor de 2.

Utilizando un único println() y las llaves de expresión ${}, imprime el siguiente mensaje calculando el total sobre la marcha:

"Has comprado 2 pares de zapatillas. El precio total es de 100 euros."

¿print o println?

Imagina que quieres que la consola muestre exactamente esto en dos líneas:

Hola Mundo
¡Amo Kotlin!

Escribe el código usando tres instrucciones de impresión para conseguir esa salida exacta. Haz que la palabra «Hola» y «Mundo» se impriman en instrucciones separadas pero se queden en la misma línea.

Soluciones a los ejercicios

¡No hagas trampas! Solo mira esto si ya has intentado resolverlos.

Presentando a Mary

fun main() {
    val nombre = "Mary"
    val edad = 20
    println("$nombre tiene $edad años")
}

Batería del móvil

fun main() {
    // Usamos 'var' porque la batería va a cambiar a lo largo del tiempo
    var bateria = 100
    println("La batería inicial es del $bateria%")
    
    bateria = 75
    println("Tras jugar, la batería es del $bateria%")
}

Calculadora de la compra inteligente

fun main() {
    val precioZapatillas = 50
    val cantidadCajas = 2
    
    // Usamos ${} para multiplicar variables dentro del mismo texto
    println("Has comprado $cantidadCajas pares de zapatillas. El precio total es de ${precioZapatillas * cantidadCajas} euros.")
}

¿print o println?

fun main() {
    print("Hola ")       // Usa print, por lo que la siguiente palabra se pega a esta. (Fíjate en el espacio al final)
    println("Mundo")     // Se pega al 'Hola ', y luego hace un salto de línea por el 'ln'
    println("¡Amo Kotlin!") // Se imprime en la nueva línea
}

Programación con Kotlin nivel básico

Primeros pasos

Tipos de datos básicos

Colecciones (listas, conjuntos y mapas)

Control de flujo

Funciones y lambdas

Clases y objetos

Seguridad contra nulos