object, companion y vararg
Al terminar: Crear un único objeto con object, funciones de la clase con companion object, objetos anónimos y funciones con vararg.
Al terminar esta lección Torneo tendrá Torneo.conEquipos("Copa", rayo, toros), para crear un torneo sin armar a mano la lista (lo usa el test), y probarás un reglamento «antiguo» (2 puntos por victoria) creado en el sitio, sin declarar una clase para él. Además sabrás qué es de verdad el object ReglamentoLiga que ya usas.
Temas: object (un solo objeto), companion object (lo que pertenece a la clase), objetos anónimos, const y vararg con Array. Cada uno lleva su ejemplo con otros datos antes de llegar a Marcador.
Tres formas de tener un objeto sin Clase(...)
Normalmente un objeto se crea con Clase(...), tantos como se quiera. Una instancia es cada objeto creado a partir de una clase. Hay tres casos en que crear instancias a mano no hace falta o no conviene:
| Forma | Para qué sirve | Cuándo se crea | Ejemplo en Marcador |
|---|---|---|---|
object Nombre | Una clase con una sola instancia, que se usa por su nombre, sin paréntesis | la primera vez que se usa | ReglamentoLiga |
companion object | Miembros que pertenecen a la clase, no a cada objeto: existen una sola vez para toda la clase | cuando se carga la clase (ver abajo) | Torneo.MINIMO_DE_EQUIPOS |
object : Interfaz { … } | Un objeto sin nombre, para un solo uso | donde se escribe | un reglamento «antiguo» de prueba |
A lo primero, una clase de la que solo existe una instancia para todo el programa, se le llama singleton. La segunda forma se llama a veces «estático»: lo que va en un companion object pertenece a la clase y no a cada objeto que se crea de ella. La tercera es una expresión de objeto (un objeto anónimo); la primera, una declaración de objeto.
Cargar la clase es el momento en que el programa lee la definición de la clase y la deja lista para usarse; ocurre automáticamente la primera vez que el programa la necesita, no hay que hacer nada.
interface Saludador {
val nombre: String
fun saludar(): String = "Hola, soy $nombre"
}
// object: una clase con UNA sola instancia. Se usa por su nombre, sin paréntesis.
object Robot : Saludador {
override val nombre = "R2"
}
class Fabrica {
// companion object: miembros de la clase, no de cada objeto.
// Se usan con el nombre de la clase: Fabrica.MAX
companion object {
const val MAX = 3 // const: constante conocida al compilar
fun crear(): String = "Nueva pieza"
}
}
fun main() {
println(Robot.saludar())
println(Fabrica.MAX)
println(Fabrica.crear())
// objeto anónimo: implementa la interfaz en el sitio, sin darle nombre de clase
val anonimo = object : Saludador { override val nombre = "Anónimo" }
println(anonimo.saludar())
// `===` pregunta si son EXACTAMENTE el mismo objeto: con un object, siempre sí
println(Robot === Robot)
}Al ejecutarlo, imprime:
Hola, soy R2
3
Nueva pieza
Hola, soy Anónimo
trueconst val solo vale para tipos básicos (números, texto, booleanos) cuyo valor se conoce al compilar, y se puede escribir tanto dentro de un companion object como dentro de un object.
vararg y Array
Una función con vararg acepta cualquier cantidad de argumentos separados por comas, en vez de una lista armada a mano. Dentro de la función, ese parámetro es un Array: una colección de tamaño fijo, con size y acceso por índice, que toList() convierte en la List normal. Las dos cosas juntas, en un companion object, con otros datos:
class Grupo(val nombre: String, val miembros: List<String>) {
companion object {
const val MINIMO = 2 // existe una sola vez para toda la clase
// vararg: acepta cualquier cantidad de String separados por comas
fun de(nombre: String, vararg miembros: String): Grupo {
// dentro, miembros es un Array: tamaño fijo, con size y acceso por índice
// require corta con un error si no se cumple lo que se pide
require(miembros.size >= MINIMO) { "Un grupo necesita $MINIMO miembros" }
return Grupo(nombre, miembros.toList()) // toList(): del Array a la List que pide Grupo
}
}
}
fun main() {
val coro = Grupo.de("Coro", "Eva", "Leo", "Sara") // tres argumentos sueltos, sin listOf
println("${coro.nombre}: ${coro.miembros}")
println("Mínimo: ${Grupo.MINIMO}") // se lee con el nombre de la clase
}Al ejecutarlo, imprime:
Coro: [Eva, Leo, Sara]
Mínimo: 2Si llamaras a Grupo.de("Dúo", "Eva"), con un solo miembro, require cortaría el programa con el mensaje Un grupo necesita 2 miembros.
Reto · Ejercicio: la pizzería
En el Kotlin Playground, fuera de tu proyecto, escribe:
- Una clase
Pizzacon un nombre y una lista de ingredientes. - Para toda la clase (no para cada pizza), el máximo de ingredientes (4) y una función
armarque recibe el nombre y cualquier cantidad de ingredientes sueltos, exige que no pasen del máximo («Una pizza lleva como mucho 4 ingredientes») y devuelve la pizza. - Un único horno para todo el programa, con una temperatura fija de 220 grados y una función que dice «Horno a 220 grados».
En main, arma una Hawaiana con jamón y piña, e imprime la pizza, el horno y el máximo. La salida tiene que ser:
Hawaiana: [jamón, piña]
Horno a 220 grados
Máximo de ingredientes: 4Ver solución
class Pizza(val nombre: String, val ingredientes: List<String>) {
companion object {
const val MAXIMO = 4 // de la clase, no de cada pizza
fun armar(nombre: String, vararg ingredientes: String): Pizza {
require(ingredientes.size <= MAXIMO) { "Una pizza lleva como mucho $MAXIMO ingredientes" }
return Pizza(nombre, ingredientes.toList())
}
}
}
// Un solo horno para todo el programa: object
object Horno {
const val GRADOS = 220
fun describir(): String = "Horno a $GRADOS grados"
}
fun main() {
val hawaiana = Pizza.armar("Hawaiana", "jamón", "piña")
println("${hawaiana.nombre}: ${hawaiana.ingredientes}")
println(Horno.describir())
println("Máximo de ingredientes: ${Pizza.MAXIMO}")
}MAXIMO y armar van en el companion object porque pertenecen a la clase; el horno es un object porque solo debe haber uno y se usa por su nombre. vararg convierte los ingredientes sueltos en un Array, y toList() lo pasa a List.
Paso · object, companion y vararg en Marcador
Objetivo. Dejar Torneo.kt con su companion object (MINIMO_DE_EQUIPOS y conEquipos) y probar en Main un reglamento anónimo. Al terminar, el test del reglamento comprueba todo el conjunto.
En la vista Android, torneo → kotlin+java tiene dos paquetes con el mismo nombre: com.sebastiannarvaez.marcador.torneo guarda el código y com.sebastiannarvaez.marcador.torneo (test) guarda los tests. Torneo.kt y Main.kt ya existen: ábrelos con doble clic.
Dónde va cada cosa:
Torneo.ktgana elcompanion objectal final de la clase, antes de su última}. Se muestra entero y sustituye al tuyo.- En
Main.kt, pega el bloque nuevo en lugar de la última}demain(la que cierra la función, al final del fichero); el resto del fichero no cambia. La líneaprintln(ReglamentoLiga)de la lección anterior se queda donde está: el bloque va justo debajo.
Los bloques llevan como título la ruta del fichero en tu proyecto. El test, ReglamentoTest.kt, lo copiarás al final, cuando todo compile: comprueba los tres puntos de la liga, que object es una única instancia, el objeto anónimo, la tabla con otro reglamento y el companion (MINIMO_DE_EQUIPOS y conEquipos).
Intenta escribirlo tú. El orden que funciona:
Torneotiene, para toda la clase y no para cada torneo,MINIMO_DE_EQUIPOS, que vale 2, yconEquipos(nombre, …), que acepta cualquier cantidad de equipos sueltos, exige al menos ese mínimo («Un torneo necesita al menos 2 equipos») y devuelve el torneo.main. Imprime el reglamento de la liga; crea en el sitio, sin darle nombre, un reglamento antiguo que da 2 puntos por victoria y 1 por empate, juega con él el Rayo FC-Toros en un torneo «Copa Antigua» y muestra los puntos del primero de la tabla; por último, el mínimo de equipos.
Tras las líneas de las lecciones anteriores, la salida debe acabar así:
Partidos en el torneo: 3
Reglamento Liga (3/1/0)
Rayo con reglas antiguas: 2 puntos
Mínimo de equipos: 2Cuando termines, abre el bloque y compara con tu código.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
Torneo.kt cambia al final (el companion object nuevo), así que va entero:
package com.sebastiannarvaez.marcador.torneo
// El torneo es lo único mutable del dominio: va acumulando partidos.
// Por eso cuida qué enseña hacia fuera.
// Por composición: el torneo TIENE un reglamento (se lo dan de fuera) en vez de
// ser un tipo de reglamento. Cambiar de reglas no obliga a cambiar de clase.
class Torneo(
val nombre: String,
equipos: List<Equipo>,
val reglamento: Reglamento = ReglamentoLiga,
) {
// Copia defensiva de lo que entra.
val equipos: List<Equipo> = equipos.toList()
// `val` NO significa inmutable: la referencia no cambia, pero la lista sí
// puede cambiar por dentro. Es `private` para que nadie de fuera la toque.
private val partidosJugados = mutableListOf<Partido>()
// Lo que sale es una COPIA de solo lectura. Si devolviéramos
// `partidosJugados` directamente, el tipo `List` engañaría: por dentro
// sigue siendo una MutableList y un `as MutableList` la vaciaría.
val partidos: List<Partido>
get() = partidosJugados.toList()
fun agregar(partido: Partido) {
require(partido.local in equipos && partido.visitante in equipos) {
"Los dos equipos deben estar inscritos en $nombre"
}
partidosJugados.add(partido)
}
fun tablaDePosiciones(): List<FilaDePosicion> = tablaDePosiciones(partidosJugados, equipos, reglamento)
fun goleadores(): List<Pair<Jugador, Int>> = tablaDeGoleadores(partidosJugados)
// `companion object`: lo que pertenece a la clase y no a cada torneo. Se usa con el
// nombre de la clase: Torneo.conEquipos(...), Torneo.MINIMO_DE_EQUIPOS.
companion object {
// `const val`: constante conocida al compilar (solo tipos básicos).
const val MINIMO_DE_EQUIPOS = 2
fun conEquipos(nombre: String, vararg equipos: Equipo): Torneo {
require(equipos.size >= MINIMO_DE_EQUIPOS) {
"Un torneo necesita al menos $MINIMO_DE_EQUIPOS equipos"
}
return Torneo(nombre, equipos.toList())
}
}
}Y las líneas nuevas de Main.kt, en lugar de la última } de main (la línea … del principio solo significa «aquí queda lo que ya tenías»: no la pegues):
…
// Objeto anónimo: implementa la interfaz al vuelo, sin declarar una clase.
val reglamentoAntiguo = object : Reglamento {
override val puntosPorVictoria = 2
override val puntosPorEmpate = 1
}
val torneoAntiguo = Torneo("Copa Antigua", listOf(rayo, toros), reglamentoAntiguo)
torneoAntiguo.agregar(alFinal)
println("Rayo con reglas antiguas: ${torneoAntiguo.tablaDePosiciones().first().puntos} puntos")
println("Mínimo de equipos: ${Torneo.MINIMO_DE_EQUIPOS}")
}Ahora, el test. ReglamentoTest.kt comprueba lo que acabas de escribir; el objeto anónimo, lo único nuevo que usa, ya aparece arriba. No hace falta que lo escribas: lo copias.
- En
torneo → kotlin+java, clic derecho sobrecom.sebastiannarvaez.marcador.torneo (test)→ New → Kotlin Class/File → File, escribeReglamentoTesty pulsa Intro. - Pulsa el botón de abajo: copia el fichero al portapapeles.
- En el fichero vacío, selecciona todo (Cmd+A) y pega (Cmd+V): lo pegado trae su propia línea
package.
Como en las lecciones anteriores, el botón solo copia al portapapeles: el fichero lo creas tú.
Punto de control
- En la Terminal de Android Studio,
./gradlew :torneo:runtermina enBUILD SUCCESSFULy las últimas líneas son:
Partidos en el torneo: 3
Reglamento Liga (3/1/0)
Rayo con reglas antiguas: 2 puntos
Mínimo de equipos: 2- La tabla de posiciones de antes no cambia: con
ReglamentoLigapor defecto, Rayo FC sigue con 6 puntos. ./gradlew :torneo:testpasa, conReglamentoTest.ktcopiado: son 54 tests en total, y el test depuntosPorde Bucles y tu primer test no ha cambiado.Puntos.ktya no contiene los números3,1y0.
Comprueba lo que entendiste
5 preguntas · necesitas 4 para completar la lección
1.¿Cuántas instancias de
ReglamentoLigahay en el programa?2.¿Para qué sirve
companion objectdentro deTorneo?3.Quieres probar un reglamento de 2 puntos por victoria una sola vez, sin declarar una clase con nombre. ¿Qué escribes?
4.En
fun conEquipos(nombre: String, vararg equipos: Equipo), ¿qué tipo tieneequiposdentro de la función y para qué sirvetoList()?5.¿Qué es verdad de
const val MINIMO_DE_EQUIPOS = 2?