Clases que se validan
Al terminar: Impedir que un objeto exista mal formado con init y require, y escribir Equipo, una clase que valida su plantilla al construirse.
Al terminar tendrás la segunda clase del torneo: Equipo, con nombre y plantilla de jugadores, que se niega a existir si el nombre está en blanco o si dos jugadores comparten dorsal. Verás cómo rechaza un equipo mal formado en el mismo instante de crearlo.
Importa porque una clase que valida al construirse te ahorra comprobar después, en cada sitio donde uses el objeto, que sus datos tienen sentido. Cuando el resto del código recibe un Equipo, ya sabe que está bien.
Temas: init y require, parámetro frente a propiedad, y toString. En la lección siguiente, Propiedades y visibilidad, le añadirás un capitán que solo el equipo puede cambiar.
Una clase que revisa sus datos
A veces los datos que le das a una clase no tienen sentido: un equipo sin nombre, una sala con capacidad negativa. Si el objeto se crea igualmente, cada función que lo use tendrá que volver a comprobarlo. Es mejor que la clase se niegue a existir: así, un objeto que existe es un objeto válido.
Tres piezas nuevas:
inites un bloque que se ejecuta una vez, justo al crear el objeto, después de recibir los datos del constructor.require(condición) { "mensaje" }comprueba una condición (lo que va entre llaves detrás de la llamada es un trozo de código que se le pasa a la función, aquí el mensaje; lo verás a fondo en Lambdas y funciones de orden superior). Si estrue, no pasa nada. Si esfalse, lanza una excepción: lanzar es provocar un error que para el programa en marcha, y la excepción es ese error. La que lanzarequirese llamaIllegalArgumentException(«argumento ilegal»: el dato que me has dado no vale) y lleva ese mensaje. Las excepciones se ven a fondo en Excepciones: try, catch y require.runCatching { … }ejecuta el bloque y, si lanza una excepción, la guarda en vez de dejar caer el programa. ConexceptionOrNull()?.messagese lee el mensaje del error, onullsi no hubo ninguno (el?.y elnullson de Nulabilidad: pide el mensaje solo si hay error). Aquí solo lo usamos para enseñar el rechazo sin que el programa se pare.
class Sala(val nombre: String, val capacidad: Int) {
// init se ejecuta al crear el objeto
init {
// require(condición) { "mensaje" }: si la condición es false, lanza
// IllegalArgumentException con ese mensaje y el objeto no llega a existir
require(capacidad > 0) { "La sala $nombre necesita capacidad mayor que 0" }
}
}
fun main() {
val buena = Sala("A1", 30) // la condición se cumple: el objeto existe
println("${buena.nombre} admite ${buena.capacidad}")
// runCatching guarda el error en vez de dejar caer el programa
val intento = runCatching { Sala("B2", 0) } // capacidad 0: require lanza
println(intento.exceptionOrNull()?.message) // el mensaje del require, o null si no falló
}A1 admite 30
La sala B2 necesita capacidad mayor que 0Parámetro frente a propiedad
Los datos que entran por los paréntesis de la clase son parámetros del constructor. Solo se convierten en propiedades (datos que el objeto guarda) si llevan val o var delante. Tres formas de recibir datos en una clase:
| Escribes | Qué es | Vive |
|---|---|---|
class Equipo(val nombre: String) | parámetro y propiedad | mientras exista el objeto |
class Equipo(plantilla: List<Jugador>) | solo parámetro | durante la construcción: lo ven init y los valores iniciales de las propiedades |
var goles: Int = 0 en el cuerpo | propiedad con valor inicial | mientras exista el objeto |
El ejemplo siguiente usa tres herramientas de las listas, que se estudian a fondo en Colecciones. Aquí las ves solas, con un ejemplo propio, para que luego no sean un misterio:
- Una lista guarda varios valores en orden. Se crea con
listOf(…), y entre<>se escribe de qué es:List<Alumno>es una lista de alumnos.lista.sizedice cuántos elementos tiene. mapNotNull { … }recorre la lista, calcula algo por cada elemento y se queda con los resultados que no sonnull. Dentro de las llaves,ites el elemento de cada vuelta.toSet()la convierte en un Set, un grupo de valores sin repetidos.
fun main() {
val notas = listOf(7, null, 9, 7) // una lista con un hueco: null es «no hay nota»
println(notas.size) // 4: cuántos elementos tiene
println(notas.mapNotNull { it }) // [7, 9, 7]: it es cada elemento; los null se descartan
println(notas.mapNotNull { it }.toSet()) // [7, 9]: un Set no guarda repetidos
}4
[7, 9, 7]
[7, 9]Con eso, comparar el tamaño de la lista con el de su toSet() dice si había repetidos: es la idea con la que Equipo detecta los dorsales repetidos.
También aparece override fun toString(). Todos los objetos de Kotlin traen de fábrica un toString() (lo reciben de un molde común a todas las clases, que entenderás en Propiedades y visibilidad), la función que decide cómo se escribe el objeto cuando lo imprimes; el de fábrica solo da el nombre de la clase y un número (Libro@3d4eac69). override («sobrescribir») significa «esto sustituye lo que ya venía de fábrica»: pones tu propio toString() y println usa el tuyo.
class Alumno(val nombre: String, val edad: Int?)
// nombre es propiedad (lleva val); alumnosIniciales es solo un parámetro (no lleva val)
class Aula(val nombre: String, alumnosIniciales: List<Alumno>) {
// init se ejecuta una vez, al crear el objeto, justo después de recibir los datos del constructor
init {
require(alumnosIniciales.size <= 3) { "El aula $nombre admite 3 alumnos como mucho" }
}
// izquierda: la propiedad nueva; derecha: el parámetro del constructor
val alumnos: List<Alumno> = alumnosIniciales
// override sustituye el toString() de fábrica: decide cómo se imprime el aula
override fun toString(): String = "$nombre (${alumnos.size} alumnos)"
}
fun main() {
val lista: List<Alumno> = listOf(Alumno("Eva", 20), Alumno("Iker", null), Alumno("Leo", 20))
val edades = lista.mapNotNull { it.edad } // como arriba, pero it es cada alumno: toma su edad y descarta los null
println(edades) // [20, 20]
println(edades.toSet()) // [20]: un Set no guarda repetidos
println(Aula("2B", lista)) // println llama a toString()
val cuatro = listOf(Alumno("Eva", 20), Alumno("Iker", null), Alumno("Leo", 20), Alumno("Nuria", 22))
val intento = runCatching { Aula("3C", cuatro) } // cuatro alumnos: require lanza
println(intento.exceptionOrNull()?.message) // el mensaje del require
}[20, 20]
[20]
2B (3 alumnos)
El aula 3C admite 3 alumnos como mucho¿Por qué no escribir directamente val alumnos en el constructor? Porque así parámetro y propiedad son dos cosas distintas, y eso permite que la propiedad guarde algo diferente de lo que llegó. Hoy guarda la misma lista; en Inmutabilidad y referencias guardará una copia defensiva (una copia propia, para que nadie desde fuera pueda cambiar la plantilla del equipo) y solo hará falta tocar esa línea.
En Aula los nombres eran distintos (alumnosIniciales y alumnos) para que se viera cuál es cuál. En Equipo, que escribirás enseguida, parámetro y propiedad se llaman igual: plantilla. Se lee así: en val plantilla: List<Jugador> = plantilla, el nombre de la izquierda es la propiedad nueva y el de la derecha, el parámetro. Como init va antes de esa línea, dentro de init plantilla es el parámetro. Después, en las funciones de la clase, plantilla es la propiedad.
Reto · Ejercicio: una reserva con aforo
En el Kotlin Playground: una reserva de restaurante es una clase Reserva con el número de mesa y la lista de nombres de los comensales. Se niega a existir si hay más de 4 comensales, con el mensaje «La mesa 5 admite 4 comensales como mucho» (el número es el de la mesa). Al imprimir una reserva debe salir «Mesa 3: 2 comensales». En main, imprime la reserva de la mesa 3 para Ana y Luis; después intenta crear la de la mesa 5 con cinco nombres cualesquiera, sin que el programa se caiga, e imprime el motivo del rechazo. La consola debe mostrar:
Mesa 3: 2 comensales
La mesa 5 admite 4 comensales como muchoVer solución
class Reserva(val mesa: Int, comensales: List<String>) {
init {
require(comensales.size <= 4) { "La mesa $mesa admite 4 comensales como mucho" }
}
val comensales: List<String> = comensales
override fun toString(): String = "Mesa $mesa: ${comensales.size} comensales"
}
fun main() {
println(Reserva(3, listOf("Ana", "Luis")))
val intento = runCatching { Reserva(5, listOf("Ana", "Luis", "Eva", "Iker", "Leo")) }
println(intento.exceptionOrNull()?.message)
}Mesa 3: 2 comensales
La mesa 5 admite 4 comensales como muchomesa lleva val porque es propiedad; comensales entra como parámetro, lo revisa init y después se guarda en la propiedad del mismo nombre. println usa el toString que sobrescribiste.
Paso · Equipo con plantilla válida
Objetivo: Equipo(nombre, plantilla) que rechaza nombres en blanco y dorsales repetidos y se imprime como «Rayo FC (3 jugadores)». Al terminar, main imprime Rayo FC (3 jugadores) y el rechazo del equipo Toros. Aún no hay tests nuevos: el proyecto sigue en 11.
Crea Equipo.kt: en la vista Android, despliega torneo → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador.torneo (el que no lleva (test)) → New → Kotlin Class/File → Class → Equipo. Android Studio escribe un esqueleto class Equipo {} debajo de package: bórralo, porque lo sustituyes por tu clase.
Intenta escribirlo tú. Un Equipo se crea con un nombre y una plantilla de jugadores (una List<Jugador>), y se niega a existir si el nombre está en blanco («El equipo necesita un nombre») o si dos jugadores comparten dorsal («Dorsales repetidos en Toros: [7, 7]», donde la lista son los dorsales de la plantilla; los jugadores sin dorsal no cuentan como repetidos). Al imprimir un equipo sale «Rayo FC (3 jugadores)». Dos pistas: nombre.isNotBlank() es true si el texto tiene algo más que espacios, y para quedarte con los dorsales que existen usa mapNotNull sobre la plantilla, como en el ejemplo del Aula; si la lista de dorsales tiene el mismo tamaño que su toSet(), no hay repetidos.
En Main.kt (en com.sebastiannarvaez.marcador.torneo), pega el bloque de abajo en lugar de la última } de main (la que cierra la función, justo debajo de println(presentar(null))): el bloque ya trae esa } al final. Crea el Rayo FC con la lista jugadores que escribiste en Clases y objetos, lo imprime, e intenta crear los Toros con dos jugadores con el dorsal 7, sin que el programa se caiga. Tras las líneas de la lección anterior, debe verse:
Rayo FC (3 jugadores)
Toros: Dorsales repetidos en Toros: [7, 7]Los bloques de abajo llevan como título la ruta del fichero en tu proyecto. El de Equipo.kt es el fichero entero; el de Main.kt es solo un fragmento: el … de arriba son las líneas que ya tienes, así que no selecciones todo, pega solo lo indicado arriba.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
package com.sebastiannarvaez.marcador.torneo
// `nombre` lleva `val` en los paréntesis de la clase: se convierte en propiedad.
// `plantilla` es solo un parámetro (sin val): existe durante la construcción.
class Equipo(val nombre: String, plantilla: List<Jugador>) {
// El bloque init corre al construir. `require` lanza IllegalArgumentException
// con el mensaje si la condición es falsa.
init {
require(nombre.isNotBlank()) { "El equipo necesita un nombre" }
val dorsales = plantilla.mapNotNull { it.dorsal }
require(dorsales.size == dorsales.toSet().size) {
"Dorsales repetidos en $nombre: $dorsales"
}
}
// izquierda: la propiedad; derecha: el parámetro
val plantilla: List<Jugador> = plantilla
// override sustituye el toString() de fábrica.
override fun toString(): String = "$nombre (${plantilla.size} jugadores)"
} …
// Clases: un equipo valida su plantilla al construirse.
val rayo = Equipo("Rayo FC", jugadores)
println(rayo)
val error = runCatching { Equipo("Toros", listOf(Jugador("A", 7), Jugador("B", 7))) }
println("Toros: ${error.exceptionOrNull()?.message}")
}Rayo FC (3 jugadores)
Toros: Dorsales repetidos en Toros: [7, 7]Un detalle que se queda a propósito: Equipo guarda la lista tal cual la recibe, sin copiarla. Es una deuda que se paga en Inmutabilidad y referencias.
Punto de control
./gradlew :torneo:runtermina conRayo FC (3 jugadores)yToros: Dorsales repetidos en Toros: [7, 7]../gradlew :torneo:testsigue enBUILD SUCCESSFUL. Abre el informe (open torneo/build/reports/tests/test/index.html, como en Bucles y tu primer test) y comprueba que arriba pone 11 tests, los 11 de antes: en esta lección no añades ninguno.- Si cambias el dorsal de uno de los jugadores del Rayo para repetir otro (por ejemplo, dos con el 9), el programa se cae al crear el equipo con
Dorsales repetidos en Rayo FC. Devuélvelo a como estaba.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.En
class Equipo(val nombre: String, plantilla: List<Jugador>), ¿qué diferencia hay entrenombreyplantilla?2.¿Qué hace
require(condicion) { "mensaje" }dentro deinit?3.¿Cuándo se ejecuta el bloque
initde una clase?4.¿Qué hace
override fun toString(): String = "..."en una clase?5.En
Equipo,dorsales.toSet().sizees menor quedorsales.size. ¿Qué significa?