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 loscompanion 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 comowhen
. - 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étodostoString
,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 sentenciabreak
,switch
sí.when
se puede utilizar para comprobar datos de un rango (1..6
),switch
no.when
es más flexible queswitch
.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
ystring
,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 dethis
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 dePersona
.- 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