Companion object en Kotlin: qué es y cómo se usa
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 objectes 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
objectdirectamente. - 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-jvm2.4.21 (zip oficial de GitHub) y OpenJDK 25.0.5, en la imagen Dockereclipse-temurin:25.