Nivel inicial14 min de lectura · Publicado el

Companion object en Kotlin: qué es y cómo se usa

Companion object en Kotlin: un tablón en el portal, MyClass1, con FLAG y welcome(), compartido por todos los pisos, que son las instancias. Companion object, uno solorubentejera.com MyClass1FLAG = 1welcome()uno solo, lo leen todospisos: instancias
Cada piso es una instancia; el tablón del portal es el companion object: hay uno solo por clase y lo leen todos. MyClass1.FLAG y MyClass1.welcome() se usan sin crear ninguna instancia.

Un companion object en Kotlin es un objeto que pertenece a una clase y que es común a todas sus instancias. Sirve para lo que en Java harías con campos y métodos static: constantes, funciones de fábrica y utilidades de la clase. Se llama con el nombre de la clase, sin crear ningún objeto.

Piensa en una comunidad de vecinos. Cada piso (cada instancia) tiene sus cosas, pero el tablón del portal es uno solo y lo leen todos. No tienes que comprarte un piso para leerlo: basta con ir al portal del edificio, que es el nombre de la clase.

Si estás empezando, mira antes qué es Kotlin.

Cómo se declara y cómo se usa

Se escribe companion object dentro de la clase y, dentro de sus llaves, lo que quieras compartir:

class MyClass1 {
    companion object {
        const val FLAG = 1

        fun welcome() = "Welcome from Companion Object!!"
    }
}

fun main() {
    println(MyClass1.FLAG)
    println(MyClass1.welcome())
    println(MyClass1.Companion.welcome())
}

Al ejecutarlo:

1
Welcome from Companion Object!!
Welcome from Companion Object!!

Fíjate en que nunca escribimos MyClass1(): no hace falta crear ninguna instancia para usar FLAG ni welcome(). La tercera línea muestra el nombre completo, MyClass1.Companion, que Kotlin pone cuando no le das otro. Casi nunca hace falta escribirlo.

const val marca una constante que se conoce al compilar, como un número o un texto fijo. Es uno de los usos más habituales del companion object: guardar las constantes de la clase.

Con nombre propio

Puedes darle un nombre. Las llamadas cortas siguen funcionando:

class Pedido(val lineas: List<String>) {
    companion object Fabrica {
        fun vacio() = Pedido(emptyList())
    }
}

fun main() {
    println(Pedido.vacio().lineas)
    println(Pedido.Fabrica.vacio().lineas)
    println(Pedido.Fabrica::class.simpleName)
}
[]
[]
Fabrica

El nombre solo se nota al llamar al código desde Java o al usar reflexión. En el día a día, Pedido.vacio() basta.

Estado compartido entre instancias

Lo que guardas en el companion object es uno solo para toda la clase. Este contador lo incrementa cada instancia nueva:

class Visita {
    init {
        total++ // cada instancia nueva suma una al contador compartido
    }

    companion object {
        var total = 0
            private set
    }
}

fun main() {
    Visita()
    Visita()
    Visita()
    println(Visita.total)
}
3

private set deja leer Visita.total desde fuera, pero solo la propia clase puede cambiarlo. Úsalo con cuidado: un valor que cambia y que comparte todo el programa es fácil de romper desde cualquier sitio, igual que una variable global.

Companion object frente a static de Java

Si vienes de Java, la pregunta es si el companion object «es» static. Se usa casi igual, pero por dentro no lo es. La documentación oficial lo explica así: aunque parezcan miembros estáticos, son miembros de instancia de un objeto real, y por eso un companion object puede implementar interfaces.

Esta tabla resume las diferencias prácticas (es explicación nuestra a partir de la documentación de Kotlin y de los ejemplos de este artículo):

static en Java companion object en Kotlin
Qué es Miembros de la clase Miembros de un objeto único que vive dentro de la clase
Cómo se llama Clase.metodo() Clase.metodo()
Desde una instancia Se permite (obj.CAMPO), aunque no se recomienda No compila (lo verás en los errores típicos)
¿Implementa interfaces? No Sí
¿Se puede pasar como parámetro? No Sí: saluda(MyClass2)
¿Cuántos por clase? Todos los miembros estáticos que quieras Uno, con todos los miembros que quieras
Inicialización Al cargar la clase Al cargar la clase, «con la misma semántica que un inicializador estático de Java», según la documentación (traducción nuestra)
Visto desde Java — Clase.Companion.metodo(), salvo const, @JvmField o @JvmStatic

Como cualquier objeto: implementar una interfaz

Como el companion object es un objeto, puede cumplir una interfaz y pasarse a una función que la espere:

interface Welcome {
    fun welcome(): String
}

class MyClass2 {
    companion object : Welcome {
        override fun welcome() = "Welcome from Companion Object + Interface!!"
    }
}

fun saluda(alguien: Welcome) = println(alguien.welcome())

fun main() {
    saluda(MyClass2) // el nombre de la clase se refiere a su companion object
}
Welcome from Companion Object + Interface!!

saluda recibe cualquier cosa que cumpla la interfaz Welcome, y le pasamos MyClass2 tal cual: el nombre de la clase, usado solo, es una referencia a su companion object. Con static de Java esto no se puede hacer, porque un método estático no pertenece a ningún objeto que pasar.

Llamarlo desde Java: @JvmStatic, @JvmField y const

Esta clase de Kotlin tiene una constante y dos funciones iguales, una con @JvmStatic y otra sin ella:

class Precios {
    companion object {
        const val IVA = 21

        fun conIva(base: Double) = base * (1 + IVA / 100.0)

        @JvmStatic
        fun conIvaEstatico(base: Double) = base * (1 + IVA / 100.0)
    }
}

Y así se usa desde Java:

public class Main {
    public static void main(String[] args) {
        System.out.println(Precios.IVA);                      // const val: campo static
        System.out.println(Precios.Companion.conIva(100.0));   // sin anotación: a través de Companion
        System.out.println(Precios.conIvaEstatico(100.0));     // @JvmStatic: método static real
    }
}
21
121.0
121.0

Si en Java intentas Precios.conIva(100.0), sin Companion, javac lo rechaza:

Main.java:3: error: cannot find symbol
        System.out.println(Precios.conIva(100.0));
                                  ^
  symbol:   method conIva(double)
  location: class Precios

Según la guía de interoperabilidad con Java, @JvmStatic genera las dos cosas: un método estático en la clase y el método normal en el companion. Para propiedades que no son constantes, @JvmField las expone como un campo estático. Si tu código solo lo usa Kotlin, no necesitas ninguna de las dos.

Companion object frente a object

object y companion object se parecen tanto que es fácil confundirlos. Los dos crean un único objeto. La diferencia está en dónde vive:

  • object Nombre (declaración de objeto) es un objeto único con nombre propio y sin clase alrededor. Es la forma de Kotlin de escribir un singleton: una clase de la que solo existe un ejemplar.
  • companion object es ese mismo objeto único, pero metido dentro de una clase que sí tiene instancias. Acompaña a la clase, de ahí su nombre.
// Declaración de objeto: un único objeto con nombre, sin clase alrededor
object Configuracion {
    val idioma = "es"
    fun saludo() = if (idioma == "es") "Hola" else "Hello"
}

// Companion object: el objeto único va dentro de una clase con instancias
class Termometro(val grados: Double) {
    companion object {
        fun desdeFahrenheit(f: Double) = Termometro((f - 32) * 5 / 9)
    }
}

fun main() {
    println(Configuracion.saludo())
    println(Configuracion === Configuracion)

    val a = Termometro(20.0)
    val b = Termometro.desdeFahrenheit(212.0)
    println("${a.grados} ${b.grados}")
    println(a === b)
}
Hola
true
20.0 100.0
false

Configuracion es siempre el mismo objeto (=== compara si dos referencias apuntan al mismo objeto; lo tienes explicado en igualdad referencial y estructural en Kotlin). En cambio, de Termometro puedes crear tantos como quieras; lo único que es uno solo es su companion.

Cuándo se inicializa cada uno

La documentación marca otra diferencia: una declaración de objeto se inicializa de forma perezosa, la primera vez que se usa, y un companion object, cuando se carga su clase. Este ejemplo lo hace visible con bloques init:

object Registro {
    init { println("  -> se inicializa Registro (object)") }
    fun usar() = "Registro listo"
}

class Pantalla {
    companion object {
        init { println("  -> se inicializa el companion de Pantalla") }
        fun abrir() = "Pantalla abierta"
    }
}

fun main() {
    println("1. Empieza main")
    println("2. " + Registro.usar())
    println("3. Creo una Pantalla")
    Pantalla()
    println("4. " + Pantalla.abrir())
}
1. Empieza main
  -> se inicializa Registro (object)
2. Registro listo
3. Creo una Pantalla
  -> se inicializa el companion de Pantalla
4. Pantalla abierta

Registro no existe hasta que se usa, y el companion de Pantalla se crea al cargar la clase, aquí al crear la primera instancia. En la práctica casi nunca importa, salvo que el init haga algo costoso o tenga efectos visibles.

object companion object
Dónde se declara En el nivel superior o dentro de otra clase u objeto, pero no dentro de una función Dentro de una clase o una interfaz
Cómo se usa Configuracion.saludo() Termometro.desdeFahrenheit(212.0)
Nombre Obligatorio Opcional (Companion por defecto)
Cuántos Los que quieras Uno por clase
Inicialización La primera vez que se usa Al cargar la clase
Para qué Un objeto único: configuración, registro, implementación sin estado Lo que es de la clase y no de cada instancia: constantes, fábricas

La tabla es un resumen nuestro de la documentación de declaraciones de objeto. Existe además la expresión de objeto (object : Interfaz { … }), que crea un objeto anónimo en el momento, sin nombre; la verás en el ejemplo de interfaces más abajo.

Funciones de fábrica: controlar cómo se crean los objetos

Con un constructor privado y una función en el companion object, decides cómo se crean los objetos y qué se comprueba antes:

class Usuario private constructor(val nombre: String) {
    companion object {
        fun crear(nombre: String): Usuario {
            require(nombre.isNotBlank()) { "El nombre no puede estar vacío" }
            return Usuario(nombre.trim())
        }
    }
}

fun main() {
    println(Usuario.crear("  Ana ").nombre)
    try {
        Usuario.crear("   ")
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
}
Ana
Error: El nombre no puede estar vacío

Nadie puede saltarse la comprobación: Usuario("Ana") no compila fuera de la clase, porque el constructor es privado. Lo tienes en el apartado de errores. Si quieres repasar private y el resto de modificadores, mira modificadores de acceso en Kotlin.

Un companion object es un objeto, no una forma de escribir static.

Otros usos: interfaces, enums y extensiones

En una interfaz. Las interfaces también pueden tener companion object, por ejemplo para ofrecer una forma de crear implementaciones:

interface Forma {
    fun area(): Double

    companion object {
        fun cuadrado(lado: Double): Forma = object : Forma {
            override fun area() = lado * lado
        }
    }
}

enum class Talla(val codigo: String) {
    PEQUENA("S"), MEDIANA("M"), GRANDE("L");

    companion object {
        fun desdeCodigo(codigo: String) = entries.first { it.codigo == codigo }
    }
}

fun main() {
    println(Forma.cuadrado(3.0).area())
    println(Talla.desdeCodigo("M"))
}
9.0
MEDIANA

En un enum. Es un sitio natural para buscar un valor a partir de un dato, como Talla.desdeCodigo("M"). Ojo: si el código no existe, first lanza una excepción; en código real, firstOrNull y devolver null suele ser más seguro.

Extensiones. Puedes añadir funciones al companion de una clase desde fuera, y se llaman como si fueran de la clase. La clase tiene que declarar un companion, aunque esté vacío:

class Color(val r: Int, val g: Int, val b: Int) {
    companion object
}

// Función de extensión sobre el companion: se llama como si fuera de la clase
fun Color.Companion.gris(nivel: Int) = Color(nivel, nivel, nivel)

fun main() {
    val c = Color.gris(128)
    println("${c.r}, ${c.g}, ${c.b}")
}
128, 128, 128

Privado. Un private companion object solo se ve desde dentro de la clase. Sirve para constantes que nadie más debe usar:

class Token(val valor: String) {
    fun esValido() = valor.length == LONGITUD

    private companion object {
        const val LONGITUD = 8
    }
}

fun main() {
    println(Token("abcdefgh").esValido())
}
true

Errores típicos

Estos son los mensajes que da kotlinc 2.4.21 en cada caso.

Declarar dos companion objects. Solo se permite uno por clase:

class Doble {
    companion object A
    companion object B
}
error: only one companion object is allowed per class.

Usarlo desde una instancia. En Java puedes leer un campo estático a través de un objeto; en Kotlin, no:

val obj = MyClass1()
println(obj.FLAG)
error: unresolved reference 'FLAG' on receiver of type 'MyClass1'.

La solución es MyClass1.FLAG.

Usar propiedades de la instancia dentro del companion. El companion no pertenece a ningún piso concreto, así que no sabe cuál es «tu» nombre:

class Saludo(val nombre: String) {
    companion object {
        fun hola() = "Hola, $nombre"
    }
}
error: unresolved reference 'nombre'.

Pásale el dato como parámetro (fun hola(nombre: String)) o convierte la función en un método normal de la clase.

Poner const val dentro de una clase. Una constante de compilación tiene que ir en un object, en un companion o fuera de cualquier clase:

class Ajustes {
    const val LIMITE = 10
}
error: const 'val' is only allowed on top level, in named objects, in companion objects or companion blocks.

Usar desde fuera un companion privado o un constructor privado:

error: cannot access 'companion object Companion : Any': it is private in 'Token'.
error: cannot access 'constructor(nombre: String): Usuario': it is private in 'Usuario'.

Cuándo no usarlo

  • Si la función no necesita nada de la clase, una función de nivel superior (escrita fuera de cualquier clase) suele ser más sencilla. Kotlin no te obliga a meterlo todo en clases, como Java.
  • Si solo quieres un objeto único, sin instancias alrededor, usa object directamente.
  • Si guardas estado que cambia (contadores, cachés), piensa si de verdad debe compartirlo todo el programa: es estado global y complica los tests.
  • Si el companion object crece mucho, esa lógica merece su propia clase. Tienes criterios para decidirlo en clases en código limpio.

Para seguir con Kotlin, tienes interfaces en Kotlin y una comparación general del lenguaje en Kotlin vs Java.

Fuentes

  • Documentación oficial de Kotlin: Object declarations and expressions, secciones Companion objects, Object declarations y diferencias de inicialización (página del 20-10-2026, consultada el 9-11-2026).
  • Documentación oficial de Kotlin: Calling Kotlin from Java, secciones Static fields y Static methods (consultada el 9-11-2026).
  • Kotlin KEEP: KEEP-0449, Companion extensions and blocks (consultada el 9-11-2026).
  • Versiones publicadas de Kotlin en GitHub: 2.4.21 (8-10-2026) es la última estable; 2.5.0-Beta2, del 21-10-2026.
  • Todos los ejemplos, incluidos los errores y la llamada desde Java, se han compilado y ejecutado el 9 de noviembre de 2026 con kotlinc-jvm 2.4.21 (zip oficial de GitHub) y OpenJDK 25.0.5, en la imagen Docker eclipse-temurin:25.