La Programación Orientada a Objetos (POO) es un paradigma de programación basado en la utilización de objetos.
Un objeto agrupa principalmente:
Por ejemplo, si queremos representar un coche podríamos tener:
Coche Propiedades: marca modelo velocidad Funciones: acelerar() frenar() mostrarInformacion()
Para definir este tipo de elementos utilizaremos clases.
Una clase es una plantilla que define las características y el comportamiento que tendrán los objetos creados a partir de ella.
Por ejemplo:
class Coche { var marca = "" var modelo = "" var velocidad = 0 }
Coche. Clase frente a objeto
Podemos entender una clase como un plano, plantilla o molde, y un objeto como ele ejemplar concreto realizado a partir de ese plano.
Una misma clase puede utilizarse para crear muchos objetos diferentes.
Para declarar una clase utilizamos la palabra reservada `class` seguida del nombre de la clase:
class Persona { }
Por convenio, los nombres de las clases comienzan con mayúscula:
class Persona class Alumno class Producto class CuentaBancaria
Una clase puede estar formada por dos partes:
Por ejemplo:
class Persona(val nombre: String) { . . . var edad = 0 fun saludar() { println("Hola, soy $nombre") } . . . }
La cabecera es:
class Persona(val nombre: String)
El cuerpo es todo lo que aparece entre `{ }`:
{ var edad = 0 . . . fun saludar() { println("Hola, soy $nombre") } . . . }
El cuerpo puede contener, entre otros elementos:
Si una clase no necesita cuerpo podemos omitir las llaves:
class Persona(val nombre: String, var edad: Int)
Por cuestiones organizativas:
class Alumno → Alumno.kt
Aunque Kotlin no lo exige, mejora mucho la organización del proyecto y hace más sencillo localizar las clases. Para ficheros que agrupen funciones u otras declaraciones relacionadas, sí tiene sentido utilizar un nombre descriptivo diferente, por ejemplo Utilidades.kt.
Para crear un objeto escribimos el nombre de la clase seguido de paréntesis:
val persona = Persona("Ana", 20)
A esta operación la denominamos instanciar una clase.
persona es ahora un objeto o instancia de Persona.
A diferencia de otros lenguajes orientados a objetos como Java, en Kotlin no se utiliza la palabra `new` para crear objetos.
Por ejemplo:
// Kotlin val persona = Persona("Ana", 20)
No escribimos:
// Incorrecto en Kotlin // val persona = new Persona("Ana", 20)
Podemos crear múltiples objetos de una misma clase:
val persona1 = Persona("Ana", 20) val persona2 = Persona("Carlos", 25) val persona3 = Persona("Marta", 19)
Todos son objetos diferentes, aunque hayan sido creados utilizando la misma clase.
Las variables que forman parte de una clase reciben normalmente el nombre de propiedades.
Por ejemplo:
class Coche { var marca = "" var modelo = "" var velocidad = 0 }
La clase `Coche` tiene tres propiedades:
marca modelo velocidad
Podemos acceder a ellas utilizando el operador . :
val coche = Coche() coche.marca = "Toyota" coche.modelo = "Corolla" coche.velocidad = 50 println(coche.marca) println(coche.modelo) println(coche.velocidad)
Al igual que las variables normales, las propiedades pueden declararse utilizando val o var.
Por ejemplo:
class Persona( val nombre: String, var edad: Int )
En este caso:
nombre es una propiedad que no puede reasignarse.edad es una propiedad que puede modificarse.val persona = Persona("Ana", 20) persona.edad = 21
Esto es correcto.
Sin embargo:
// ERROR // persona.nombre = "Marta"
no está permitido porque `nombre` fue declarado mediante `val`.
Al igual que con las variables normales, es recomendable utilizar `val` cuando una propiedad no necesite ser reasignada.
Crea una clase Libro con las propiedades titulo, autor y numeroPaginas. Crea dos objetos diferentes de la clase, da valor a sus propiedades y después muestra sus propiedades por pantalla.
Cuando creamos un objeto normalmente queremos proporcionar valor a sus propiedades. Para ello utilizamos un constructor. La forma más habitual en Kotlin es utilizar un constructor primario, que se declara directamente en la cabecera de la clase:
class Persona(val nombre: String, var edad: Int) { }
Ahora debemos proporcionar esos valores al crear el objeto:
val persona = Persona("Ana", 20)
Un constructor primario permite que los parámetros que indiquemos, se conviertan automáticamente en propiedades de la clase, y podemos acceder a ellas:
println(persona.nombre) println(persona.edad)
Resultado:
Ana 20
La sintaxis completa del constructor podría escribirse utilizando la palabra `constructor`:
class Persona constructor(val nombre: String, var edad: Int) { }
Sin embargo, normalmente podemos omitirla:
class Persona(val nombre: String, var edad: Int) { }
Esta es la forma que utilizaremos habitualmente.
Crea una clase Alumno cuyo constructor reciba:
nombre, como propiedad val.curso, como propiedad val.nota, como propiedad var.Crea un alumno, modifica posteriormente su nota y muestra todos sus datos.
Existe una diferencia importante entre escribir:
class Persona(nombre: String)
y:
class Persona(val nombre: String)
En el primer caso nombre se pasa como parámetro al constructor. En el segundo caso `nombre` se convierte en una propiedad del objeto.
Por ello podemos hacer:
val persona = Persona("Ana") println(persona.nombre)
Sin `val` o `var`, el parámetro del constructor no se convierte automáticamente en una propiedad accesible del objeto.
Por tanto:
class Persona( val nombre: String, var edad: Int )
es una forma muy habitual de declarar simultáneamente:
Observa las siguientes clases:
class Usuario1(nombre: String) class Usuario2(val nombre: String)
Crea un objeto de cada clase e intenta ejecutar:
println(usuario1.nombre) println(usuario2.nombre)
¿Cuál de las dos instrucciones produce un error? Explica brevemente por qué.
Los parámetros del constructor pueden tener valores por defecto:
class Persona( val nombre: String = "Desconocido", var edad: Int = 0 )
Ahora podemos crear objetos de diferentes maneras:
val persona1 = Persona() val persona2 = Persona("Ana") val persona3 = Persona("Carlos", 25)
Los valores serán:
persona1 -> nombre = "Desconocido", edad = 0 persona2 -> nombre = "Ana", edad = 0 persona3 -> nombre = "Carlos", edad = 25
También podemos utilizar argumentos con nombre:
val persona = Persona( edad = 25, nombre = "Carlos" )
No todas las propiedades tienen que declararse en el constructor. Podemos declarar otras propiedades dentro del cuerpo de la clase:
class Persona( val nombre: String, val anioNacimiento: Int ) { ``` val mayoriaEdad = 18 var activo = true ``` }
También podemos calcular una propiedad utilizando los datos recibidos por el constructor:
class Rectangulo( val base: Double, val altura: Double ) { ``` val area = base * altura ``` }
Uso:
val rectangulo = Rectangulo(10.0, 5.0) println(rectangulo.area)
Resultado:
50.0
Crea una clase Circulo que reciba mediante el constructor su radio. Añade en el cuerpo de la clase una propiedad area cuyo valor se calcule automáticamente:
PI * radio * radio
Crea un círculo y muestra su área.
En ocasiones necesitamos ejecutar instrucciones cuando se crea un objeto.
Para ello podemos utilizar un bloque `init`.
class Persona( val nombre: String, var edad: Int ) { ``` init { println("Creando una nueva persona") } ``` }
Cuando hacemos:
val persona = Persona("Ana", 20)
se ejecutará automáticamente el bloque `init`.
Podemos acceder desde él a los parámetros y propiedades:
class Persona( val nombre: String, var edad: Int ) { ``` init { println("Creando a $nombre") println("Edad inicial: $edad") } ``` }
Uno de los usos del bloque init es comprobar que los datos utilizados para crear un objeto sean correctos.
Por ejemplo:
class Persona( val nombre: String, var edad: Int ) { ``` init { require(edad >= 0) { "La edad no puede ser negativa" } } ``` }
Esto sería válido:
val persona = Persona("Ana", 20)
Pero:
val persona = Persona("Ana", -5)
provocaría un error porque no se cumple la condición establecida mediante `require()`.
Otro ejemplo:
class Producto( val nombre: String, val precio: Double ) { . . . init{ require(nombre.isNotBlank()) { "El nombre no puede estar vacío" } require(precio >= 0) { "El precio no puede ser negativo" } } . . . }
require() permite indicar una condición que debe cumplirse.
Si la condición es falsa, Kotlin lanza una excepción indicando que el argumento proporcionado no es válido.
No es necesario profundizar todavía en las excepciones para poder utilizar esta validación básica.
Crea una clase Articulo con las propiedades nombre y precio. Utiliza un bloque init y require() para impedir que se puedan crear artículos con el nombre vacío o con un precio negativo.
Comprueba su funcionamiento.
Una clase puede tener además constructores secundarios.
Se declaran mediante constructor dentro del cuerpo de la clase.
Por ejemplo:
class Persona( val nombre: String, var edad: Int ) { ``` constructor(nombre: String) : this(nombre, 0) ``` }
Ahora podemos crear:
val persona1 = Persona("Ana", 20) val persona2 = Persona("Carlos")
En el segundo caso se utiliza el constructor secundario:
constructor(nombre: String) : this(nombre, 0)
que llama al constructor primario proporcionando:
nombre -> valor recibido edad -> 0
Si existe un constructor primario, los constructores secundarios deben terminar delegando en él mediante `this(…)`.
No obstante, Kotlin permite solucionar muchas situaciones mediante valores por defecto, por lo que no necesitaremos constructores secundarios con tanta frecuencia como en otros lenguajes.
Por ejemplo, en el caso anterior probablemente sea más sencillo escribir:
class Persona( val nombre: String, var edad: Int = 0 )
Las clases también pueden contener funciones.
Estas funciones representan el comportamiento de los objetos.
Por ejemplo:
class Coche( val marca: String, val modelo: String, var velocidad: Int ) { ``` fun acelerar() { velocidad += 10 } fun frenar() { velocidad -= 10 } fun acelerar(cantidad : Int) { velocidad += cantidad } fun mostrarInformacion() { println("$marca $modelo - $velocidad km/h") } ``` }
Podemos utilizar estas funciones mediante el operador . :
fun main() { ``` val coche = Coche("Toyota", "Corolla", 0) coche.acelerar() coche.acelerar() coche.mostrarInformacion() ``` }
Resultado:
Toyota Corolla - 20 km/h
Las funciones pueden acceder directamente a las propiedades de su objeto:
fun acelerar() { velocidad += 10 }
No necesitamos pasar `velocidad` como parámetro porque pertenece al propio objeto.
Crea una clase CuentaBancaria con las propiedades titular y saldo. Añade dos funciones:
ingresar(cantidad): aumenta el saldo.mostrarSaldo(): muestra el saldo actual.Crea una cuenta y da valor a las dos propiedades, realiza dos ingresos y muestra el saldo resultante.
Las data classes son clases destinadas principalmente a almacenar datos. Se declaran utilizando data class:
data class Usuario( val nombre: String, val edad: Int )
A partir de las propiedades del constructor primario, Kotlin genera automáticamente varias funciones útiles:
| Función | Finalidad |
|---|---|
toString() | Devuelve una representación del objeto y sus propiedades. |
equals() | Permite comparar dos objetos por sus datos. |
hashCode() | Genera un código asociado a los datos del objeto. |
copy() | Permite crear una copia del objeto. |
componentN() | Permite desestructurar el objeto. |
Por ejemplo:
val usuario1 = Usuario("Ana", 25) val usuario2 = Usuario("Ana", 25) println(usuario1) println(usuario1 == usuario2) val usuario3 = usuario1.copy(edad = 30) println(usuario3)
Salida:
Usuario(nombre=Ana, edad=25) true Usuario(nombre=Ana, edad=30)
Para declarar una data class, su constructor primario debe contener al menos una propiedad y sus parámetros deben declararse como val o var.
Crea una data class denominada Pelicula con las propiedades:
titulodirectoranio
Crea dos películas con los mismos datos y comprueba mediante == si Kotlin las considera iguales. Crea después una tercera película utilizando copy(), modificando únicamente el año.
Una data class también puede tener propiedades declaradas dentro del cuerpo de la clase:
data class Persona(val nombre: String) { var edad: Int = 0 }
Sin embargo, las funciones generadas automáticamente por Kotlin solo tienen en cuenta las propiedades declaradas en el constructor primario.
En el ejemplo anterior, nombre se utiliza en equals(), toString(), copy(), etc., mientras que edad queda excluida.
Por ejemplo:
val persona1 = Persona("Ana") val persona2 = Persona("Ana") persona1.edad = 20 persona2.edad = 35 println(persona1 == persona2)
El resultado será:
true
Los dos objetos se consideran iguales porque equals() únicamente tiene en cuenta nombre, ya que es la única propiedad declarada en el constructor primario.
En una data class, debemos colocar en el constructor primario las propiedades que representan los datos principales del objeto y que queremos que participen en operaciones como la comparación, copia o representación del objeto.
Una variable que contiene un objeto contiene una referencia a ese objeto. Por ejemplo:
val producto1 = Producto( "Teclado", 50.0, 10 ) val producto2 = producto1
En este caso no hemos creado dos productos independientes.
`producto1` y `producto2` hacen referencia al mismo objeto.
Por tanto:
producto2.stock = 5 println(producto1.stock)
mostrará:
5
Esto es diferente de lo que ocurre con tipos sencillos como `Int`:
var numero1 = 10 var numero2 = numero1 numero2 = 20 println(numero1)
El resultado continúa siendo:
10
Comprender que los objetos se manejan mediante referencias será importante posteriormente cuando trabajemos con objetos, colecciones y aplicaciones Android.
Observa el siguiente código:
class Caja(var contenido: Int) val caja1 = Caja(10) val caja2 = caja1 caja2.contenido = 50 println(caja1.contenido)
¿Qué valor mostrará el programa?
Explica brevemente por qué modificar caja2 afecta también a caja1.
Vamos a representar un producto de una tienda.
Cada producto tendrá:
Además podrá:
class Producto( val nombre: String, val precio: Double, var stock: Int = 0 ) { . . . init { require(nombre.isNotBlank()) { "El nombre no puede estar vacío" } require(precio >= 0) { "El precio no puede ser negativo" } require(stock >= 0) { "El stock no puede ser negativo" } } fun vender(cantidad: Int) { if (cantidad <= 0) { println("La cantidad debe ser positiva") } else if (cantidad > stock) { println("No existe stock suficiente") } else { stock -= cantidad println("Venta realizada") } } fun reponer(cantidad: Int) { if (cantidad > 0) { stock += cantidad } } fun calcularValorStock(): Double { return precio * stock } fun mostrarInformacion() { println("Producto: $nombre") println("Precio: $precio €") println("Stock: $stock") println("Valor del stock: ${calcularValorStock()} €") } . . . }
Podemos utilizarla:
fun main() { . . . val teclado = Producto( nombre = "Teclado mecánico", precio = 59.95, stock = 10 ) teclado.mostrarInformacion() teclado.vender(3) teclado.reponer(5) teclado.mostrarInformacion() }
En este ejemplo podemos identificar claramente los dos elementos principales de un objeto.
Estado del objeto:
nombre precio stock
Comportamiento del objeto:
vender() reponer() calcularValorStock() mostrarInformacion()
Crea una clase denominada:
Vehiculo
Cada vehículo debe almacenar:
Por ejemplo:
val coche = Vehiculo( matricula = "1234 ABC", marca = "Toyota", modelo = "Corolla", kilometros = 50000, combustible = 30.0, capacidadDeposito = 50.0 )
La clase debe proporcionar las siguientes funciones:
mostrarInformacion() repostar(litros) realizarViaje(kilometros, consumo)
La función `repostar()` debe comprobar:
Por ejemplo, si quedan 40 litros en un depósito de 50 y hacemos:
coche.repostar(20.0)
el depósito deberá quedar en:
50 litros
La función realizarViaje(kilometros, consumo) recibirá:
Por ejemplo:
coche.realizarViaje( kilometros = 200, consumo = 6.5 )
El programa deberá calcular el combustible necesario:
combustibleNecesario = kilometros * consumo / 100
Si existe combustible suficiente:
Si no existe combustible suficiente:
Además, utiliza un bloque `init` para comprobar que:
Finalmente, crea al menos tres vehículos diferentes y realiza distintas operaciones sobre ellos.
Crea una clase Alumno que almacene:
nombre apellidos edad notas
Las notas pueden almacenarse en:
val notas = mutableListOf<Double>()
La clase deberá proporcionar las funciones:
agregarNota(nota) calcularMedia() estaAprobado() mostrarInformacion()
agregarNota() solamente deberá aceptar valores entre `0` y `10`.calcularMedia() devolverá la media de las notas del alumno.estaAprobado() devolverá un `Boolean` indicando si la media es igual o superior a `5`.mostrarInformacion() deberá producir una salida similar a:Alumno: Ana García Edad: 20 Notas: 7.5, 8.0, 6.5, 9.0 Media: 7.75 Resultado: APROBADO
Crea posteriormente varios objetos `Alumno`, añade diferentes notas y muestra sus resultados.
Kotlin proporciona diferentes tipos de colecciones para almacenar y gestionar conjuntos de datos.
Las principales son:
| Colección | Características |
|---|---|
List | Colección ordenada de elementos. Permite elementos repetidos y acceso mediante índices. |
Set | Colección de elementos únicos. No permite elementos duplicados. |
Map | Almacena parejas formadas por una clave y un valor. Cada clave es única. |
Las colecciones pueden ser de solo lectura o mutables:
Una List almacena una colección ordenada de elementos. Cada elemento ocupa una posición o índice, comenzando por 0.
Podemos crear una lista utilizando listOf():
val nombres = listOf("Ana", "Luis", "Marta", "Pedro")
Podemos acceder a sus elementos mediante su índice:
println(nombres[0]) println(nombres[2])
También podemos consultar algunas propiedades y funciones:
println(nombres.size) println(nombres.first()) println(nombres.last()) println(nombres.contains("Marta"))
Una lista creada con listOf() es de solo lectura, por lo que no podemos modificar su contenido:
// Error nombres.add("Laura")
| Método | Descripción | Ejemplo |
|---|---|---|
add(elemento) | Añade un elemento al final de la lista | lista.add(“Ana”) |
add(indice, elemento) | Inserta un elemento en una posición concreta | lista.add(1, “Luis”) |
addAll(coleccion) | Añade varios elementos | lista.addAll(otraLista) |
remove(elemento) | Elimina la primera aparición del elemento indicado | lista.remove(“Ana”) |
removeAt(indice) | Elimina el elemento situado en una posición | lista.removeAt(0) |
clear() | Elimina todos los elementos | lista.clear() |
contains(elemento) | Comprueba si un elemento está en la lista | lista.contains(“Ana”) |
indexOf(elemento) | Devuelve la posición de un elemento, o -1 si no existe | lista.indexOf(“Ana”) |
isEmpty() | Indica si la lista está vacía | lista.isEmpty() |
size | Devuelve el número de elementos | lista.size |
get(indice) | Devuelve el elemento de una posición | lista.get(0) |
set(indice, elemento) | Sustituye el elemento de una posición | lista.set(0, “Pedro”) |
Cuando necesitamos añadir, modificar o eliminar elementos podemos utilizar mutableListOf():
val nombres = mutableListOf("Ana", "Luis", "Marta")
Podemos añadir elementos mediante add():
nombres.add("Pedro")
También podemos modificar un elemento utilizando su índice:
nombres[1] = "Carlos"
Para eliminar elementos podemos utilizar remove() o removeAt():
// Elimina el elemento indicado nombres.remove("Marta") // Elimina el elemento situado en la posición indicada nombres.removeAt(0)
También podemos eliminar todos los elementos:
nombres.clear()
A diferencia de los arrays, cuyo tamaño se establece al crearlos, una MutableList puede cambiar de tamaño durante la ejecución del programa al añadir o eliminar elementos.
Es habitual crear inicialmente una lista vacía e ir añadiendo elementos durante la ejecución del programa.
Para crear una lista mutable vacía debemos indicar el tipo de elementos que almacenará:
val alumnos = mutableListOf<Alumno>()
Por ejemplo, utilizando una data class:
data class Alumno( val nombre: String, val edad: Int ) val alumnos = mutableListOf<Alumno>() alumnos.add(Alumno("Ana", 20)) alumnos.add(Alumno("Luis", 22)) alumnos.add(Alumno("Marta", 19)) for (alumno in alumnos) { println("${alumno.nombre} - ${alumno.edad} años") }
También podemos eliminar objetos de la lista:
alumnos.removeAt(1)
o buscar un objeto y eliminarlo:
val alumno = alumnos.find { it.nombre == "Ana" } // la función find() puede devolver null, así que compruebo if (alumno != null) { alumnos.remove(alumno) }
La forma más sencilla de recorrer una lista es mediante un bucle for:
val nombres = mutableListOf("Ana", "Luis", "Marta") for (nombre in nombres) { println(nombre) }
Si necesitamos conocer también el índice podemos utilizar indices:
for (i in nombres.indices) { println("$i - ${nombres[i]}") }
Por ejemplo:
0 - Ana 1 - Luis 2 - Marta
Crea la siguiente data class:
data class Videojuego( val titulo: String, val precio: Double )
Crea una lista mutable vacía de videojuegos, añade tres videojuegos y recorre posteriormente la lista mostrando el título y precio de cada uno.
Internamente, las colecciones pueden recorrerse mediante un iterador (Iterator), que permite acceder secuencialmente a sus elementos.
Cuando utilizamos un bucle for, Kotlin utiliza un iterador de forma implícita:
for (nombre in nombres) { println(nombre) }
También podemos crear el iterador explícitamente:
val iterador = nombres.iterator() while (iterador.hasNext()) { println(iterador.next()) }
En las colecciones mutables, un MutableIterator permite además eliminar elementos mientras recorremos la colección:
val numeros = mutableListOf(10, 20, 30, 40) val iterador = numeros.iterator() while (iterador.hasNext()) { val numero = iterador.next() if (numero == 20) { iterador.remove() } } println(numeros)
Resultado:
[10, 30, 40]
Kotlin permite ordenar los elementos de una MutableList mediante diferentes funciones.
Para ordenar una lista de números o cadenas podemos utilizar:
sort(): ordena los elementos de forma ascendente (de menor a mayor o alfabéticamente).sortDescending(): ordena los elementos de forma descendente (de mayor a menor o en orden alfabético inverso).Por ejemplo:
val numeros = mutableListOf(8, 3, 10, 1, 5) numeros.sort() println(numeros) // [1, 3, 5, 8, 10] numeros.sortDescending() println(numeros) // [10, 8, 5, 3, 1]
Ambas funciones modifican directamente la lista original.
Cuando tenemos una lista de objetos, podemos ordenarla utilizando el valor de alguna de sus propiedades.
Por ejemplo, partimos de la siguiente data class:
data class Videojuego( val titulo: String, val precio: Double )
Creamos una lista mutable de videojuegos:
val videojuegos = mutableListOf( Videojuego("Minecraft", 29.99), Videojuego("Hollow Knight", 14.99), Videojuego("Mario Kart", 49.99) )
Para ordenar los videojuegos por precio, de menor a mayor:
videojuegos.sortBy { it.precio }
Para ordenarlos por precio, de mayor a menor:
videojuegos.sortByDescending { it.precio }
También podemos ordenarlos alfabéticamente por título:
videojuegos.sortBy { it.titulo }
En estos ejemplos, it representa cada objeto Videojuego de la lista, mientras que it.precio o it.titulo indica la propiedad utilizada para ordenar.
Si quiero obtener una nueva lista ordenada sin modificar la original, la siguiente función me devuelve una nueva lista:
val ordenados = videojuegos.sortedBy { it.precio }
val lenguajes = mutableListOf("Kotlin", "Java", "Python") // Añadir lenguajes.add("C#") // Modificar lenguajes[1] = "JavaScript" // Eliminar lenguajes.remove("Python") // Recorrer for (lenguaje in lenguajes) { println(lenguaje) }
Utilizaremos List cuando únicamente necesitemos consultar los datos y MutableList cuando necesitemos añadir, modificar o eliminar elementos.
Partiendo de una lista mutable de nombres:
val nombres = mutableListOf("Ana", "Luis", "Marta", "Pedro")
Realiza las siguientes operaciones:
Laura.Luis.Marta por María.Recorre y muestra la lista resultante.
Un Set es una colección que no permite elementos duplicados.
Podemos crear uno mediante setOf():
val numeros = setOf(10, 20, 30, 20) println(numeros)
Resultado:
[10, 20, 30]
Para poder modificar su contenido utilizamos mutableSetOf():
val numeros = mutableSetOf(10, 20, 30) numeros.add(40) numeros.remove(20) for (numero in numeros) { println(numero) }
Un Map almacena datos mediante parejas clave-valor.
Por ejemplo, podemos relacionar el nombre de un alumno con su nota:
val notas = mapOf( "Ana" to 8, "Luis" to 6, "Marta" to 9 ) println(notas["Ana"])
Para modificar su contenido utilizamos mutableMapOf():
val notas = mutableMapOf( "Ana" to 8, "Luis" to 6 ) // Añadir notas["Marta"] = 9 // Modificar notas["Luis"] = 7 // Eliminar notas.remove("Ana")
Podemos recorrer sus claves y valores mediante:
for ((nombre, nota) in notas) { println("$nombre tiene un $nota") }
| Necesidad | Colección recomendada |
|---|---|
| Elementos ordenados y accesibles mediante índice | List / MutableList |
| Elementos sin duplicados | Set / MutableSet |
| Asociar claves con valores | Map / MutableMap |
Crear una aplicación que me permita almacenar elementos de tipo Alumno en una lista. Los Alumnos tendrán nombre y apellidos, dni, y calificación.
El programa me permitirá mediante un menú, almacenar nuevos alumnos pidiéndome sus datos, listar todos los alumnos, eliminar alumnos a partir del dni, listar alumnos aprobados, y modificar calificación de un alumno a partir de su dni.
Como en programas anteriores, el programa no debe terminar hasta que en el menú de texto seleccione la opción de salir del programa.
Una excepción es un problema que se produce durante la ejecución de un programa y que puede interrumpir su funcionamiento normal.
Por ejemplo, pueden producirse excepciones al:
Dividir un número entero entre cero.
Acceder a una posición inexistente de un array.
Convertir a número una cadena que no contiene un valor numérico.
Intentar utilizar incorrectamente un valor null.
Por ejemplo:
val numero = "Hola".toInt()
La conversión no puede realizarse y se producirá una excepción de tipo NumberFormatException.
En Kotlin, las excepciones derivan de la clase Exception, que a su vez deriva de Throwable.
Podemos controlar una excepción mediante los bloques try y catch.
En try escribimos el código que puede producir la excepción.
En catch indicamos qué hacer si se produce.
try { val numero = "Hola".toInt() println(numero) } catch (e: NumberFormatException) { println("No se ha podido convertir el texto a un número") }
Si la conversión produce una NumberFormatException, la ejecución pasa al bloque catch en lugar de finalizar bruscamente el programa.
Un ejemplo habitual al solicitar datos al usuario sería:
try { print("Introduce tu edad: ") val edad = readln().toInt() println("Tu edad es $edad") } catch (e: NumberFormatException) { println("Debes introducir un número entero") }
Un mismo bloque try puede tener varios bloques catch:
try { val numeros = arrayOf(10, 20, 30) print("Introduce una posición: ") val posicion = readln().toInt() println(numeros[posicion]) } catch (e: NumberFormatException) { println("Debes introducir un número") } catch (e: IndexOutOfBoundsException) { println("La posición no existe") }
Cada catch se encarga de un tipo concreto de excepción.
Opcionalmente podemos añadir un bloque finally.
El código contenido en finally se ejecutará siempre, tanto si se produce una excepción como si no.
try { val numero = readln().toInt() println("Número: $numero") } catch (e: NumberFormatException) { println("Valor incorrecto") } finally { println("Fin de la operación") }
La estructura general es:
try { // Código que puede producir una excepción } catch (e: TipoDeExcepcion) { // Tratamiento de la excepción } finally { // Código que se ejecuta siempre }
El bloque finally es especialmente útil cuando necesitamos liberar recursos independientemente de si una operación termina correctamente o produce una excepción.
Solicita al usuario dos números enteros y realiza su división. Utiliza try-catch para controlar:
NumberFormatException si el usuario no introduce un número.ArithmeticException si intenta dividir entre cero.Muestra un mensaje diferente para cada error.
También podemos provocar nosotros mismos una excepción utilizando throw.
Por ejemplo:
fun establecerEdad(edad: Int) { if (edad < 0) { throw IllegalArgumentException("La edad no puede ser negativa") } println("Edad: $edad") }
Al ejecutar lo siguiente, se lanzará una IllegalArgumentException.
establecerEdad(-5)
| Función de precondición | Utilidad | Excepción generada |
|---|---|---|
require() | Comprueba que los argumentos o datos de entrada sean válidos. | IllegalArgumentException |
check() | Comprueba que el estado de un objeto o variable sea válido. | IllegalStateException |
error() | Indica que el programa ha alcanzado un estado o condición no válida. | IllegalStateException |
Kotlin proporciona la función require() para comprobar que un argumento cumple una determinada condición.
Por ejemplo:
class Persona( val nombre: String, val edad: Int ) { init { require(edad >= 0) } }
Si intentamos crear:
val persona = Persona("Ana", -5)
require() lanzará automáticamente una IllegalArgumentException. Esta forma resulta especialmente útil para comprobar que los datos recibidos por una función o constructor son válidos.
La función require() y la función check()permite al compilador realizar una conversión inteligente de tipos (smart cast).
Después de que la comprobación se realice correctamente, la variable se convierte automáticamente a un tipo que no admite valores nulos (non-nullable).
Esta función se utiliza frecuentemente para realizar comprobaciones de nulabilidad y garantizar que una variable no contiene null antes de continuar con su utilización.
Por ejemplo:
fun imprimirCadenaNoNula(texto: String?) { // Comprobamos que texto no sea null require(texto != null) // Después de la comprobación, Kotlin sabe que 'texto' // no puede ser null y realiza automáticamente un smart cast // de String? a String println(texto.length) }
Algunas excepciones que podemos encontrar frecuentemente son:
| Excepción | Situación habitual |
|---|---|
ArithmeticException | Operación aritmética no válida, como una división entera entre cero. |
NumberFormatException | Conversión a número de una cadena con formato incorrecto. |
IndexOutOfBoundsException | Acceso a una posición inexistente de un array o colección. |
NoSuchElementException | Intento de obtener un elemento que no existe. |
IllegalArgumentException | Se proporciona un argumento que no cumple las condiciones requeridas. |
IllegalStateException | El objeto o programa se encuentra en un estado no válido para realizar una operación. |
NullPointerException | Se intenta utilizar una referencia null de forma no segura. |
Aunque podemos capturar excepciones mediante try-catch, siempre que sea posible es preferible evitar que se produzcan.
Kotlin proporciona diferentes funciones para realizar operaciones de forma segura.
Por ejemplo, en lugar de:
val numero = texto.toInt()
podemos utilizar:
val numero = texto.toIntOrNull()
Si la conversión no es posible, se obtiene null en lugar de producirse una NumberFormatException.
De forma similar, para acceder de forma segura a una colección podemos utilizar:
val elemento = lista.getOrNull(posicion)
Las excepciones deben utilizarse para gestionar situaciones excepcionales. No deberían sustituir a las comprobaciones normales que podemos realizar en nuestro programa.
El siguiente programa solicita dos números enteros y realiza una división:
fun main() { try { print("Introduce el dividendo: ") val dividendo = readln().toInt() print("Introduce el divisor: ") val divisor = readln().toInt() val resultado = dividendo / divisor println("Resultado: $resultado") } catch (e: NumberFormatException) { println("Debes introducir números enteros") } catch (e: ArithmeticException) { println("No se puede dividir entre cero") } finally { println("Operación finalizada") } }
El valor null representa la ausencia de un valor.
Uno de los problemas habituales en muchos lenguajes de programación es intentar acceder a una propiedad o función de una variable cuyo valor es null, provocando una excepción NullPointerException (NPE).
Kotlin incorpora un sistema denominado Null Safety que diferencia entre variables que pueden contener null y variables que no pueden contenerlo.
Por defecto, las variables en Kotlin no pueden contener null.
Por ejemplo:
var nombre: String = "Ana" // Error de compilación nombre = null
La variable nombre es de tipo String, por lo que siempre debe contener un valor válido de ese tipo.
Esto permite utilizar sus propiedades y funciones directamente:
println(nombre.length)
Si queremos permitir que una variable pueda contener null, debemos añadir ? al tipo:
var nombre: String? = "Ana" nombre = null
Por tanto:
String → No puede contener null String? → Puede contener un String o null Int → No puede contener null Int? → Puede contener un Int o null
Cuando una variable es nulable, Kotlin no permite acceder directamente a sus propiedades:
val nombre: String? = null // Error // println(nombre.length)
Antes debemos gestionar la posibilidad de que contenga null.
La forma más sencilla consiste en comprobar explícitamente el valor:
val nombre: String? = "Ana" if (nombre != null) { println(nombre.length) }
Después de comprobar que nombre no es null, Kotlin sabe que dentro del bloque puede tratarlo como un String normal.
Esto es un ejemplo de smart cast:
String? → String
Kotlin proporciona el operador ?. para acceder de forma segura a una propiedad o función.
val nombre: String? = "Ana" println(nombre?.length)
Si nombre contiene un valor, se ejecutará:
nombre.length
Si nombre es null, la expresión devolverá null en lugar de producir una excepción.
Por ejemplo:
val nombre: String? = null println(nombre?.length)
Resultado:
null
También podemos encadenar varias llamadas seguras:
persona?.direccion?.ciudad?.nombre
Si cualquiera de los elementos de la cadena es null, el resultado completo será null.
El operador Elvis ?: permite proporcionar un valor alternativo cuando una expresión devuelve null.
Por ejemplo:
val nombre: String? = null val nombreMostrar = nombre ?: "Desconocido" println(nombreMostrar)
Resultado:
Desconocido
También podemos combinarlo con ?.:
val nombre: String? = null val longitud = nombre?.length ?: 0
Esto significa:
nombre no es null, obtiene su longitud.nombre es null, utiliza 0.Por tanto, esta expresión:
val longitud = nombre?.length ?: 0
es una forma más concisa de escribir:
val longitud = if (nombre != null) { nombre.length } else { 0 }
El operador !! indica a Kotlin que estamos seguros de que una variable no contiene null.
val nombre: String? = "Ana" println(nombre!!.length)
Kotlin tratará nombre como un valor no anulable.
Sin embargo, si realmente contiene null:
val nombre: String? = null println(nombre!!.length)
se producirá una NullPointerException.
El operador !! elimina la protección que proporciona el sistema de Null Safety de Kotlin.
Debe utilizarse únicamente cuando podamos garantizar que el valor no será null. Siempre que sea posible, es preferible utilizar ?., ?: o realizar una comprobación explícita.
| Sintaxis | Significado |
|---|---|
String | El valor no puede ser null. |
String? | El valor puede ser un String o null. |
variable != null | Comprueba explícitamente que el valor no sea null. |
?. | Accede de forma segura. Si el valor es null, devuelve null. |
?: | Proporciona un valor alternativo cuando el resultado es null. |
!! | Fuerza a tratar el valor como no nulo. Puede producir una NullPointerException. |
Una vez que conocemos las características de las variables nulables, podemos añadir un nuevo mecanismo a la hora de validar datos leídos desde teclado.
Con las siguientes funciones puedo asegurarme de que el dato introducido es el esperado, o null en caso contrario:
| Descripción | Tipo devuelto | Funcionamiento |
|---|---|---|
toByteOrNull() | Byte? | Convierte el texto a Byte. Devuelve null si no es válido o está fuera de rango. |
toShortOrNull() | Short? | Convierte el texto a Short. Devuelve null si no es válido o está fuera de rango. |
toIntOrNull() | Int? | Convierte el texto a Int. Devuelve null si no es válido o está fuera de rango. |
toLongOrNull() | Long? | Convierte el texto a Long. Devuelve null si no es válido o está fuera de rango. |
toFloatOrNull() | Float? | Convierte el texto a Float. Devuelve null si no tiene un formato numérico válido. |
toDoubleOrNull() | Double? | Convierte el texto a Double. Devuelve null si no tiene un formato numérico válido. |
toBooleanStrictOrNull() | Boolean? | Convierte “true” o “false” a Boolean. Devuelve null para cualquier otro valor. |
Un ejemplo muy útil para explicar su relación con Null Safety sería:
print("Introduce tu edad: ") val edad = readln().toIntOrNull() if (edad == null) { println("Debes introducir un número entero válido") } else { println("Tu edad es $edad") }
También puedes combinarlo con el operador Elvis ?:
print("Introduce tu edad: ") val edad = readln().toIntOrNull() ?: 0 println("Edad: $edad")
Para validar que el usuario ha introducido exactamente un carácter se puede utilizar, por ejemplo, singleOrNull():
val caracter: Char? = readln().singleOrNull()
Así, “A” devuelve el carácter 'A', mientras que una entrada como “Hola” devuelve null.
fun mostrarUsuario(nombre: String?, edad: Int?) { ``` val nombreMostrar = nombre ?: "Desconocido" println("Nombre: $nombreMostrar") if (edad != null) { println("Edad: $edad") println("El año que viene tendrá ${edad + 1} años") } else { println("Edad no especificada") } ``` } fun main() { ``` mostrarUsuario("Ana", 20) mostrarUsuario(null, null) ``` }
Como regla general, debemos intentar utilizar tipos no anulables siempre que sea posible y declarar un tipo como nullable (?) únicamente cuando la ausencia de un valor sea una situación válida dentro de nuestro programa.
Solicita al usuario su edad y utiliza:
val edad = readln().toIntOrNull()
Si edad contiene null, muestra:
Edad incorrecta
En caso contrario, indica si el usuario es mayor o menor de edad.
No utilices el operador !!.
La herencia permite crear una clase a partir de otra existente, reutilizando y especializando su comportamiento.
La clase de la que se hereda se denomina clase padre o superclase, mientras que la nueva clase se denomina clase hija o subclase.
Por ejemplo, podemos tener una clase general Animal y crear clases más específicas como Perro y Gato.
Por defecto, las clases en Kotlin son final, lo que significa que por defecto no se puede heredar de ellas. Para permitir la herencia debemos utilizar la palabra open:
open class Animal(val nombre: String) { fun comer() { println("$nombre está comiendo") } }
Para crear una clase que herede de Animal utilizamos el caracter ':'
class Perro(nombre: String) : Animal(nombre) { fun ladrar() { println("$nombre está ladrando") } }
Ahora Perro dispone tanto de sus propios miembros como de los heredados de Animal:
val perro = Perro("Toby") perro.comer() perro.ladrar()
En este ejemplo:
Animal → superclase Perro → subclase
Una subclase puede proporcionar su propia implementación de un método heredado.
Para permitirlo, el método de la clase padre debe declararse como open:
open class Animal(val nombre: String) { open fun hacerSonido() { println("El animal hace un sonido") } }
La clase hija utiliza override para sobrescribirlo:
class Perro(nombre: String) : Animal(nombre) { override fun hacerSonido() { println("$nombre dice: Guau!") } }
Por ejemplo:
val perro = Perro("Toby") perro.hacerSonido()
Salida:
Toby dice: Guau!
En Kotlin, las clases y sus métodos son final por defecto.
Debemos utilizar:
open para permitir que una clase pueda heredarse.open para permitir que un método pueda sobrescribirse.override para sobrescribir un método heredado.
Las propiedades también pueden declararse como open y sobrescribirse en las clases hijas:
open class Animal { open val sonido = "Sonido desconocido" } class Perro : Animal() { override val sonido = "Guau" }
La palabra super permite acceder a miembros de la clase padre desde una clase hija.
open class Animal { open fun hacerSonido() { println("Sonido de animal") } } class Perro : Animal() { override fun hacerSonido() { super.hacerSonido() println("Guau") } }
En este caso, super.hacerSonido() ejecuta primero la implementación definida en Animal.
Una misma clase padre puede tener diferentes clases hijas:
open class Animal(val nombre: String) { open fun hacerSonido() { println("Sonido de animal") } } class Perro(nombre: String) : Animal(nombre) { override fun hacerSonido() { println("$nombre dice: Guau") } } class Gato(nombre: String) : Animal(nombre) { override fun hacerSonido() { println("$nombre dice: Miau") } }
Podemos tratar objetos de las clases hijas como objetos de la clase padre:
val animales = arrayOf( Perro("Toby"), Gato("Misi") ) for (animal in animales) { animal.hacerSonido() }
Salida:
Toby dice: Guau Misi dice: Miau
Aunque todos los elementos se tratan como Animal, cada objeto ejecuta su propia versión de hacerSonido(). Este comportamiento es una de las bases del polimorfismo.
La herencia establece una relación de tipo “es un”.
Un Perro es un Animal, por lo que la herencia tiene sentido.
Sin embargo, no deberíamos utilizar herencia únicamente para reutilizar código cuando no existe esta relación entre las clases.
Crea una clase Vehiculo con una propiedad marca y una función mostrarInformacion().
Vehiculo de la marca Toyota
Crea dos clases Coche y Moto que hereden de Vehiculo y sobrescriban mostrarInformacion() para mostrar:
Coche de la marca Toyota Moto de la marca . . .
Recuerda utilizar open y override donde corresponda.
Después, dentro de una función main, crearé un objeto de cada tipo e invocaré dicha función.
Para reconocer si un objeto pertenece a una clase en Kotlin, usamos el operador is:
animal : Animal = Perro("Toby"); if(animal is Perro){ println("Es un objeto de tipo Perro") } También puedo usar la negación ''!is'', si quiero comprobar lo contrario.
Una clase abstracta es una clase diseñada para servir como base para otras clases.
Se declara utilizando la palabra abstract:
abstract class Animal { // ... }
Una clase abstracta no puede ser instanciada directamente:
abstract class Animal // Error val animal = Animal()
Su objetivo es definir características y comportamientos comunes que serán heredados por otras clases.
Una clase abstracta puede contener métodos abstractos, es decir, métodos que se declaran pero no proporcionan una implementación.
abstract class Animal(val nombre: String) { abstract fun hacerSonido() }
Las clases que hereden de Animal deberán implementar el método utilizando override:
class Perro(nombre: String) : Animal(nombre) { override fun hacerSonido() { println("$nombre dice: Guau") } } class Gato(nombre: String) : Animal(nombre) { override fun hacerSonido() { println("$nombre dice: Miau") } }
Podemos utilizar estas clases normalmente:
val perro = Perro("Toby") val gato = Gato("Misi") perro.hacerSonido() gato.hacerSonido()
Una clase abstracta puede combinar miembros abstractos, que deberán ser implementados por las clases hijas, con miembros que ya tienen una implementación.
abstract class Animal(val nombre: String) { abstract fun hacerSonido() fun dormir() { println("$nombre está durmiendo") } }
En este caso:
hacerSonido() es abstracto y cada clase hija debe implementarlo.dormir() ya tiene una implementación que pueden utilizar todas las clases hijas.Las propiedades también pueden ser abstractas:
abstract class Animal { abstract val nombre: String abstract fun hacerSonido() } class Perro( override val nombre: String ) : Animal() { override fun hacerSonido() { println("$nombre dice: Guau") } }
Los miembros declarados como abstract no necesitan utilizar open, ya que están diseñados para ser sobrescritos obligatoriamente por las clases hijas.
Una clase abstracta resulta útil cuando varias clases comparten características comunes, pero queremos dejar parte de su comportamiento para que sea definido por cada clase hija.
Por ejemplo:
Animal
|
+-------+-------+
| |
Perro Gato
| |
hacerSonido() hacerSonido()
"Guau" "Miau"
Una clase abstracta permite definir una base común para varias clases, estableciendo qué propiedades o métodos deben implementar obligatoriamente sus subclases.
Crea una clase abstracta Figura que contenga:
abstract fun calcularArea(): Double
Crea una clase Rectangulo que herede de Figura y reciba base y altura.
Desde una función main voy a ejecutar su función.
© 2026 Fernando Valdeón