Saltar al contenido
K
Kotlin: el lenguajePaso a paso30 min de lectura · 5 min de máquina Intermedio

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:

FormaPara qué sirveCuándo se creaEjemplo en Marcador
object NombreUna clase con una sola instancia, que se usa por su nombre, sin paréntesisla primera vez que se usaReglamentoLiga
companion objectMiembros que pertenecen a la clase, no a cada objeto: existen una sola vez para toda la clasecuando se carga la clase (ver abajo)Torneo.MINIMO_DE_EQUIPOS
object : Interfaz { … }Un objeto sin nombre, para un solo usodonde se escribeun 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.

ejemplo mínimo: object, companion y objeto anónimo (con comentarios)
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
true

const 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:

ejemplo mínimo: vararg en un companion object (con comentarios)
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: 2

Si 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:

  1. Una clase Pizza con un nombre y una lista de ingredientes.
  2. Para toda la clase (no para cada pizza), el máximo de ingredientes (4) y una función armar que 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.
  3. 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: 4
Ver solución
solución: la pizzería
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.

torneo → kotlin+java
com.sebastiannarvaez.marcador.torneo
Torneo.ktse modifica
Main.ktse modifica
com.sebastiannarvaez.marcador.torneo (test)
ReglamentoTest.ktse crea (se copia)
Dos ficheros modificados y un test que se copia, en la vista Android.

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:

  1. Torneo.kt gana el companion object al final de la clase, antes de su última }. Se muestra entero y sustituye al tuyo.
  2. En Main.kt, pega el bloque nuevo en lugar de la última } de main (la que cierra la función, al final del fichero); el resto del fichero no cambia. La línea println(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:

  1. Torneo tiene, para toda la clase y no para cada torneo, MINIMO_DE_EQUIPOS, que vale 2, y conEquipos(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.
  2. 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: 2

Cuando 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:

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Torneo.kt
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):

torneo/src/main/java/com/sebastiannarvaez/marcador/torneo/Main.kt
    …
    // 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.

  1. En torneo → kotlin+java, clic derecho sobre com.sebastiannarvaez.marcador.torneo (test) → New → Kotlin Class/File → File, escribe ReglamentoTest y pulsa Intro.
  2. Pulsa el botón de abajo: copia el fichero al portapapeles.
  3. En el fichero vacío, selecciona todo (Cmd+A) y pega (Cmd+V): lo pegado trae su propia línea package.
ReglamentoTest.kt

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:run termina en BUILD SUCCESSFUL y 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 ReglamentoLiga por defecto, Rayo FC sigue con 6 puntos.
  • ./gradlew :torneo:test pasa, con ReglamentoTest.kt copiado: son 54 tests en total, y el test de puntosPor de Bucles y tu primer test no ha cambiado.
  • Puntos.kt ya no contiene los números 3, 1 y 0.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Cuántas instancias de ReglamentoLiga hay en el programa?

  2. 2.¿Para qué sirve companion object dentro de Torneo?

  3. 3.Quieres probar un reglamento de 2 puntos por victoria una sola vez, sin declarar una clase con nombre. ¿Qué escribes?

  4. 4.En fun conEquipos(nombre: String, vararg equipos: Equipo), ¿qué tipo tiene equipos dentro de la función y para qué sirve toList()?

  5. 5.¿Qué es verdad de const val MINIMO_DE_EQUIPOS = 2?

Faltan 5 de 5