Tema 0: Introducción a Kotlin

Objetivos de este tema

  • Comprender los fundamentos del lenguaje Kotlin, sus características y ventajas frente a otros lenguajes como Java.
  • Escribir y ejecutar programas básicos en Kotlin, utilizando la función principal main.
  • Diferenciar entre variables mutables (var) e inmutables (val) y comprender su importancia para la gestión de la inmutabilidad.
  • Aplicar el manejo seguro de nulos (null safety) para prevenir errores comunes como NullPointerException.
  • Definir y utilizar clases, incluidas data class, sealed class, y clases con constructores primarios y secundarios.
  • Entender y utilizar objetos (object) y los companion object para crear singletons y miembros estáticos.
  • Declarar y trabajar con funciones, incluidas las funciones de extensión.
  • Gestionar colecciones (listas, mapas y conjuntos) tanto inmutables como mutables.
  • Crear y utilizar enumerados (enum) para definir conjuntos de constantes con propiedades asociadas.
  • Comprender el uso del operador Elvis (?:) y el manejo de estructuras de control como when.
  • Desarrollar un pensamiento crítico y seguro al escribir código Kotlin, utilizando prácticas modernas y efectivas.

0.1. Introducción

Kotlin es un lenguaje moderno y conciso, diseñado para ser seguro y fácil de usar. Es el lenguaje oficial de Android y combina perfectamente con Java.

0.2. Primer programa en Kotlin

1fun main(args: Array<String>) {
2    println("Hello World!")
3}
  • La función main es el punto de entrada.
  • println imprime un mensaje en consola.

0.3. Expresividad

Kotlin reduce de manera significativa el código repetitivo de Java.

Java:

 1public class Artista {
 2    private long id;
 3    private String nombre;
 4    private String url;
 5    private String mbid;
 6
 7    public long getId() { return id; }
 8    public void setId(long id) { this.id = id; }
 9
10    public String getNombre() { return nombre; }
11    public void setNombre(String nombre) { this.nombre = nombre; }
12
13    public String getUrl() { return url; }
14    public void setUrl(String url) { this.url = url; }
15
16    public String getMbid() { return mbid; }
17    public void setMbid(String mbid) { this.mbid = mbid; }
18
19    @Override
20    public String toString() {
21        return "Artista{id=" + id + ", nombre='" + nombre + '\'' +
22               ", url='" + url + '\'' + ", mbid='" + mbid + '\'' + '}';
23    }
24}

Kotlin:

1data class Artista(
2    var id: Long,
3    var nombre: String,
4    var url: String,
5    var mbid: String
6)
  • Una data class genera de manera automática los métodos toString, equals, hashCode, etc.

0.4. Variables y tipos

1val i: Int = 42      // Constante inmutable
2var mutableNum = 10  // Variable mutable
3val d: Double = i.toDouble()
4val c: Char = 'c'
5val iFromChar = c.code
  • val define constantes, var permite cambios.
  • Los tipos se infieren de manera automática, es decir, no siempre es necesario declararlos.

Operaciones bitwise (bit a bit)

1val bitwiseOr = FLAG1 or FLAG2
2val bitwiseAnd = FLAG1 and FLAG2
  • En Kotlin se usan operadores simbólicos.

Acceso y recorrido de cadenas

1val s = "Ejemplo"
2val c = s[3]  // Accede a 'm'
3val s2 = "Example"
4for (c2 in s2) print(c2)  // Recorre cada carácter

Null safety

En Java, el código generalmente es defensivo, por lo que se debe comprobar en todo momento que no se produzca un null y prevenir NullPointerException. Kotlin, en cambio, es null safety, lo que significa que se puede definir si un objeto puede o no ser null utilizando para ello el operador ?. Fíjate en los siguientes ejemplos.

 1fun main(args: Array<String>) {
 2    // No compilaría, Artista no puede ser nulo.
 3    var notNullArtista: Artista = null
 4
 5    // Artista puede ser nulo.
 6    val artista: Artista? = null
 7
 8    // No compilará, artista podría ser nulo.
 9    artista.toString()
10
11    // Mostrará por pantalla artista si es distinto de nulo.
12    artista?.toString()
13
14    // No necesitaríamos utilizar el operador ? si previamente
15    // comprobamos si es nulo.
16    if (artista != null) {
17        artista.toString()
18    }
19    // Esta operación la utilizaremos si estamos completamente seguros
20    // que no será nulo. En caso contrario se producirá una excepción.
21    artista!!.toString()
22
23    // También podríamos utilizar el operador Elvis (?:) para dar
24    // una alternativa en caso que el objeto sea nulo.
25    val nombre = artista?.nombre ?: "vacío"
26}
  • La doble exclamación (!!) se utiliza para indicar al compilador que ese objeto no será nulo, evitando así posibles comprobraciones.

0.5. Control de flujo en Kotlin

Kotlin incluye diversas estructuras de control de flujo, como todos los lenguajes, que permiten gestionar la ejecución del código según condiciones, repeticiones o casos específicos.

if - else

Forma básica:

1val a = 5
2val b = 10
3if (a > b) {
4    println("a es mayor que b")
5} else {
6    println("a no es mayor que b")
7}

Como expresión, devolviendo un valor:

1val max = if (a > b) a else b
2println("El máximo es $max")

when

La sentencia when podría decirse que es el equivalente de switch en Java, aunque con algunas diferencias:

  • when no necesita la sentencia break, switch sí.
  • when se puede utilizar para comprobar datos de un rango (1..6), switch no.
  • when es más flexible que switch.
  • when permite la verificación de tipos, switch no.
  • when permite diferentes tipos de verificación de tipos de datos, switch no.
  • switch tiene limitaciones y solo admite tipos primitivos, enum y string, when no.

Forma básica:

1val x = 3
2when (x) {
3    1 -> println("Uno")
4    2 -> println("Dos")
5    3 -> println("Tres")
6    else -> println("Otro número")
7}

Con múltiples condiciones y rangos:

1when (x) {
2    1, 2 -> println("Uno o Dos")
3    in 3..5 -> println("Entre 3 y 5")
4    else -> println("Otro")
5}

Como expresión (devolviendo un valor):

1val mensaje = when (x) {
2    1 -> "Uno"
3    2 -> "Dos"
4    else -> "Otro"
5}
6println(mensaje)

for

Recorrer una lista:

1val lista = listOf("A", "B", "C")
2for (item in lista) {
3    println(item)
4}

Recorrer un rango:

1for (i in 1..5) {
2    println(i)
3}

Con índices:

1for ((index, value) in lista.withIndex()) {
2    println("Elemento $index: $value")
3}

while

1var contador = 0
2while (contador < 3) {
3    println("Contador: $contador")
4    contador++
5}

do-while

1var numero = 0
2do {
3    println("Número: $numero")
4    numero++
5} while (numero < 3)

Otras sentencias útiles

  • break: Sale de un bucle:
1for (i in 1..5) {
2    if (i == 3) break
3    println(i)
4}
  • continue: Salta a la siguiente iteración:
1for (i in 1..5) {
2    if (i == 3) continue
3    println(i)
4}
  • return: Finaliza la ejecución de una función y devuelve un valor:
1fun obtenerValor(x: Int): String {
2    if (x < 0) return "Negativo"
3    return "No negativo"
4}

0.6. Clases y constructores

 1class Persona(nombre: String, apellido: String) {
 2    var nombre: String = nombre
 3        set(value) { field = if (value.isEmpty()) "Sin nombre" else value }
 4        get() = "Nombre: $field"
 5
 6    var apellido: String = apellido
 7        set(value) { field = if (value.isEmpty()) "Sin apellido" else value }
 8        get() = "Apellido: " + field.uppercase()
 9
10    var edad: Int = 0
11        set(value) { field = if (value < 0) 0 else value }
12
13    var anyo: Int = 0
14
15    constructor(nombre: String, apellido: String, edad: Int) : this(nombre, apellido) { this.edad = edad }
16    constructor(nombre: String, apellido: String, edad: Int, anyo: Int) : this(nombre, apellido, edad) { this.anyo = anyo }
17}
  • El uso de constructor permite definir constructores primarios y secundarios. El uso de this en el contructor tras los dos puntos (:) invoca al constructor en línea de la clase.
  • field accede al respaldo interno de la propiedad cuando se sobrecargan los getters y setters.

init

También se dispone en Kotlin del bloque init, es un inicializador que se ejecutará de manera automática al crearse una instancia de la clase, inmediatamente después del constructor primario. Es especialmente útil para realizar operaciones de inicialización complejas, validaciones o cálculos adicionales con los parámetros del constructor. Se puede declarar más de un bloque init, y se ejecutarán en el orden en que aparecen en el código. Aunque las propiedades pueden inicializarse directamente, init permite incluir lógica adicional.

 1class Persona(nombre: String, apellido: String) {
 2    var nombreCompleto: String
 3
 4    init {
 5        // Se ejecutará tras el constructor primario
 6        nombreCompleto = "$nombre $apellido"
 7        println("Inicializando Persona: $nombreCompleto")
 8    }
 9}
10
11fun main() {
12    val persona = Persona("Patricia", "Aracil")
13    println(persona.nombreCompleto)
14}

Salida esperada:

1Inicializando Persona: Patricia Aracil
2Patricia Aracil

Herencia

Por defecto, las clases en Kotlin son final, es decir, no se pueden heredar. Para permitir la herencia, se debe marcar explícitamente con open.

 1open class Persona(val nombre: String) {
 2    fun presentarse() = println("Hola, soy $nombre")
 3}
 4
 5class Estudiante(nombre: String, val curso: String) : Persona(nombre) {
 6    fun mostrarCurso() = println("Curso: $curso")
 7}
 8
 9fun main() {
10    val estudiante = Estudiante("Carlos", "2º DAM")
11    estudiante.presentarse()      // Hereda de Persona
12    estudiante.mostrarCurso()
13}
  • open class permite que otra clase herede de Persona.
  • Estudiante heredará las propiedades y funciones públicas de Persona.

0.7. Data classes y desestructuración

1data class Person(val name: String, val surname: String, val age: Int)
2val (nombre, apellido, edad) = Person("Javier", "Carrasco", 45)
  • La desestructuración permite extraer valores fácilmente.

0.8. Sealed classes

 1sealed class Vehiculo(var nRuedas: Int)
 2data class Motocicleta(var ruedas: Int = 2) : Vehiculo(ruedas)
 3data class Turismo(var ruedas: Int = 4, var puertas: Int = 2) : Vehiculo(ruedas)
 4
 5fun tipoVehiculo(vehiculo: Vehiculo): String {
 6    return when (vehiculo) {
 7        is Motocicleta -> "Es del tipo Motocicleta"
 8        is Turismo -> "Es del tipo Turismo"
 9    }
10}

Las sealed classes en Kotlin permiten restringir el número de subclases que una clase puede tener (heredar), asegurando así, que todas las subclases se declaren en el mismo archivo. Esto proporciona mayor seguridad en tiempo de compilación, ya que el compilador sabe todos los posibles tipos de subclases y puede verificar el uso exhaustivo de estas en expresiones como when. Son ideales para representar jerarquías cerradas, donde cada tipo o variante está bien definido y no se puede extender fuera del contexto previsto. Un ejemplo común es cuando se desea modelar un estado finito o conjunto limitado de resultados como una respuesta de red o un tipo de mensaje.

0.9. Pair

1val parUno = Pair("Hola", "Mundo")
2val parDos = Pair("Adiós amigos", 150)
3val (usuario, contrasenya) = Pair("javier", "kotlin")
  • Útil para devolver dos valores.

0.10. Funciones

1fun sayHello() = "Hi!"  // Compacta
2fun sayHello(name: String, surname: String) = "Hello $name $surname"

Extensiones

1fun Fragment.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
2    Toast.makeText(this.context, message, duration).show()
3}
  • Permite extender clases sin heredar.

0.11. Colecciones

Listas

1val list = listOf("A", "B", "C")  // Inmutable
2val mutableList = mutableListOf("A", "B")
3mutableList.add("C")

Mapas

1val map = mapOf(1 to "Uno", 2 to "Dos")
2val mutableMap = mutableMapOf(1 to "Uno")
3mutableMap[2] = "Dos"

Sets

1val set = setOf(1, 2, 3)
2val mutableSet = mutableSetOf(1, 2)
3mutableSet.add(3)

0.12. Enumerados

 1enum class DiasSemana(val numero: Int, val estado: String) {
 2    LUNES(1, "Trabajando"),
 3    MARTES(2, "Trabajando"),
 4    MIERCOLES(3, "Trabajando"),
 5    JUEVES(4, "Trabajando"),
 6    VIERNES(5, "Trabajando"),
 7    SABADO(6, "Descanso"),
 8    DOMINGO(7, "Descanso")
 9}
10DiasSemana.values().forEach { println("${it.numero} - ${it.name} - ${it.estado}") }

0.13. Objetos y Companion Object

Un object permite declarar un objeto como una única instancia (singleton) sin necesidad de definir una clase y crear instancias separadas. Es ideal para definir constantes, utilidades, o estructuras que no requieran múltiples copias. Los objetos también pueden tener propiedades, funciones, inicializadores (init) e incluso implementar interfaces. Además, se pueden utilizar para definir companion objects, que actuarán como miembros estáticos compartidos entre todas las instancias de una clase. Esto facilita organizar código relacionado y compartirlo de forma global, sin perder las ventajas del enfoque orientado a objetos de Kotlin.

1object MiObjeto {
2    val usuario = "Javier"
3    val base_URL = "https://miweb.com"
4    fun mostrar() = println("Función de MiObjeto")
5}

Los companion object son un objeto declarado dentro de una clase que permite definir miembros estáticos, es decir, propiedades y métodos compartidos por todas las instancias de la clase. Funciona como acompañante (de ahí el nombre) a la clase que lo contiene, y permite acceder a sus miembros directamente a través del nombre de la clase, sin necesidad de crear instancias de esta. Puede resultar útil para crear, constantes o utilidades comunes, manteniendo una sintaxis clara.

1class Empleados(val nombre: String, val apellido: String) {
2    var idEmpleado: Int
3    init { println("Init clase Empleado"); idEmpleado = numEmpleados++ }
4    companion object { var numEmpleados = 0 }
5}

0.14. Funciones avanzadas para colecciones en Kotlin

Kotlin ofrece potentes herramientas funcionales para manipular colecciones de forma eficiente, legible y concisa. A continuación, se muestran las funciones más relevantes con ejemplos detallados.

filter

Filtra elementos que cumplan una condición específica.

1val numeros = listOf(1, 2, 3, 4, 5)
2val pares = numeros.filter { it % 2 == 0 }
3println(pares) // [2, 4]

Descripción: Selecciona elementos para los que la condición es verdadera.

map

Transforma cada elemento.

1val nombres = listOf("Ana", "Luis", "Eva")
2val mayusculas = nombres.map { it.uppercase() }
3println(mayusculas) // [ANA, LUIS, EVA]

Descripción: Aplica una transformación a cada elemento.

sortedBy y sortedDescending

Ordena elementos por un criterio.

1val personas = listOf(Persona("Ana", "Pérez"), Persona("Luis", "Gómez"))
2val ordenadas = personas.sortedBy { it.nombre }

Descripción: Ordena ascendente o descendente por una clave.

groupBy

Agrupa elementos por una clave.

1val animales = listOf("gato", "perro", "gallina", "caballo")
2val agrupados = animales.groupBy { it.first() }

Descripción: Agrupa por el resultado de una función clave.

any y all

Comprueba condiciones.

1val edades = listOf(18, 20, 25)
2println(edades.any { it >= 21 }) // true
3println(edades.all { it >= 18 }) // true

Descripción: any verifica si alguno cumple, all si todos cumplen.

count

Cuenta elementos que cumplen una condición.

1println(edades.count { it >= 21 }) // 1

distinct y distinctBy

Elimina duplicados.

1val duplicados = listOf(1, 2, 2, 3, 3, 3)
2println(duplicados.distinct()) // [1, 2, 3]

take y drop

Selecciona o descarta elementos.

1val lista = listOf(1, 2, 3, 4, 5)
2println(lista.take(3)) // [1, 2, 3]
3println(lista.drop(2)) // [3, 4, 5]

zip y unzip

Combina listas.

1val nombres = listOf("Ana", "Luis")
2val edades = listOf(25, 30)
3val combinados = nombres.zip(edades)
4println(combinados) // [(Ana, 25), (Luis, 30)]

flatten

Aplana listas de listas, es decir, convierte una lista de listas (colección anidada) en una única lista.

1val listas = listOf(listOf(1, 2), listOf(3, 4))
2println(listas.flatten()) // [1, 2, 3, 4]

reduce y fold

Acumulan elementos.

1val suma = numeros.reduce { acc, num -> acc + num }
2val sumaConInicial = numeros.fold(10) { acc, num -> acc + num }

Ejemplo combinando funciones avanzadas

Ahora se combinarán varias funciones para transformar una lista de números:

 1val numeros = listOf(5, 3, 8, 1, 9, 2)
 2
 3val resultado = numeros
 4    .filter { it % 2 != 0 }           // Solo impares
 5    .sortedDescending()               // Orden descendente
 6    .map { it * 2 }                   // Multiplica por 2
 7    .take(2)                          // Toma los dos primeros
 8    .fold(0) { acc, num -> acc + num } // Suma acumulativa
 9
10println("Resultado: $resultado")
  • Lista inicial: [5, 3, 8, 1, 9, 2]
  • Paso 1 (filter): [5, 3, 1, 9]
  • Paso 2 (sortedDescending): [9, 5, 3, 1]
  • Paso 3 (map): [18, 10, 6, 2]
  • Paso 4 (take): [18, 10]
  • Paso 5 (fold): 0 + 18 + 10 = 28

Resultado final: 28

Fuentes