Companion Objects

In Java, static lets you attach fields and methods to a class itself rather than to any particular instance — think utility methods, constants, and factory methods. Kotlin has no static keyword at all. Instead it gives you the companion object: a single, class-scoped object declared inside a class that holds everything you’d normally reach for static to do — and more, since unlike a Java static block a companion object is a real object that can implement interfaces and be passed around. Once you see how it’s actually compiled, the syntax and its rules stop feeling arbitrary.

Overview / How it works

Every top-level construct in Kotlin belongs to an instance, a package, or an object — there is no bare “class-level” storage the way Java’s static provides. A companion object is Kotlin’s answer: it is an object declaration (a singleton) nested directly inside a class, and the compiler guarantees exactly one instance of it exists for the lifetime of the program, created lazily the first time the class is touched. You reach its members using the class name itself, as if they were static: ClassName.member, with no need to name the companion object explicitly.

Under the hood, the Kotlin compiler generates a real nested class for the companion object (by default named Companion, or your chosen name), and the outer class holds a single static-ish reference to one instance of it. When you write User.create("Alice"), the compiler rewrites that to something equivalent to User.Companion.create("Alice") on the JVM. If you annotate a companion member with @JvmStatic, the compiler additionally emits a true JVM static method so Java callers can use it without going through Companion at all — purely an interop convenience, it changes nothing from the Kotlin side.

Three properties make companion objects more powerful than Java statics:

  • They can implement interfaces. A companion object can implement an interface (commonly a factory interface), which means ClassName itself can be passed anywhere that interface is expected, via ClassName.Companion or a named companion.
  • They can access private members of the enclosing class, including a private constructor — this is what makes the “static factory method” pattern (hiding a constructor and forcing callers through a validated factory function) work cleanly in Kotlin.
  • There is exactly one per class. Declaring a second companion object in the same class is a compile error, unlike Java where you can freely add as many static members as you like scattered through a class.

A companion object is still just an object living inside the class — its properties are shared across every instance of the class, the same way Java statics are shared. That single fact explains most of the surprises beginners hit, covered below in Common Mistakes.

Syntax

class ClassName {
    companion object [Name] [: SuperType] {
        // properties and functions, reached as ClassName.member
    }
}
Form Meaning
companion object { ... } Unnamed companion. Members accessed as ClassName.member; the object’s own name defaults to Companion.
companion object Name { ... } Named companion. Members still accessed as ClassName.member, and the object itself as ClassName.Name.
companion object : Interface { ... } The companion implements Interface, so it can be used polymorphically wherever that interface is expected.
@JvmStatic Applied to a companion function or property; emits a true JVM static member so Java code can call it without Companion.
const val A compile-time constant. Legal at the top level or inside an object/companion object, never inside a plain class body.

Examples

Example 1: a factory method for a class with a private constructor

class User private constructor(val name: String, val id: Int) {
    companion object {
        private var nextId = 1

        fun create(name: String): User {
            val user = User(name, nextId)
            nextId++
            return user
        }
    }
}

fun main() {
    val alice = User.create("Alice")
    val bob = User.create("Bob")
    println("${alice.name} has id ${alice.id}")
    println("${bob.name} has id ${bob.id}")
}

Output:

Alice has id 1
Bob has id 2

The constructor is private, so nothing outside User can call User(...) directly — only code textually inside the class, including its companion object, has access. This forces every caller through create, which is free to run validation or, as here, assign a generated id before handing back the instance. This is Kotlin’s idiomatic replacement for Java’s private-constructor-plus-static-factory pattern.

Example 2: constants and a companion utility function

class Circle(val radius: Double) {
    companion object {
        const val PI = 3.14159

        fun areaOf(radius: Double): Double = PI * radius * radius
    }

    fun area(): Double = PI * radius * radius
}

fun main() {
    println("PI = ${Circle.PI}")
    val c = Circle(2.0)
    println("Area = ${c.area()}")
    println("Area via companion = ${Circle.areaOf(3.0)}")
}

Output:

PI = 3.14159
Area = 12.56636
Area via companion = 28.274309999999996

PI is declared const val inside the companion object, which inlines it as a true compile-time constant — the same role Java’s public static final double PI plays, except const only works on top-level or object/companion members, never inside a regular class body (see Common Mistakes). Notice area(), an instance method, reads PI with no qualifier at all: any member of a class can see its own companion object’s members directly, because the companion is lexically part of the class.

Example 3: a named companion object implementing an interface

interface JsonFactory<T> {
    fun fromJson(json: String): T
}

class Point(val x: Int, val y: Int) {
    override fun toString(): String = "Point(x=$x, y=$y)"

    companion object Factory : JsonFactory<Point> {
        override fun fromJson(json: String): Point {
            val body = json.removePrefix("{").removeSuffix("}")
            val parts = body.split(",")
            val x = parts[0].split(":")[1].trim().toInt()
            val y = parts[1].split(":")[1].trim().toInt()
            return Point(x, y)
        }
    }
}

fun main() {
    val p = Point.fromJson("{x:3, y:5}")
    println(p)

    val factory: JsonFactory<Point> = Point.Factory
    println(factory.fromJson("{x:10, y:20}"))
}

Output:

Point(x=3, y=5)
Point(x=10, y=20)

Naming the companion Factory and having it implement JsonFactory<Point> means Point.Factory is a genuine value of type JsonFactory<Point> — it can be stored in a variable, passed to a function expecting any JsonFactory, or swapped for a different implementation. A Java static method could never do this: it isn’t a value, it can’t implement an interface, and it can’t be passed around polymorphically. Note Point.fromJson(...) still works without mentioning Factory — the class name always forwards to the companion regardless of its name.

How it works step by step

  • The first time ClassName is referenced anywhere in the program (a static-like access, or creating an instance), the JVM class-loads it and, as part of that, constructs the single companion instance.
  • The compiler generates a nested class for the companion (named Companion by default) and gives the outer class a reference to its one instance.
  • ClassName.member is resolved by the compiler to ClassName.Companion.member (or, for an @JvmStatic member, to a genuine bytecode-level static call).
  • Code written inside ClassName (instance methods, init blocks, other companion functions) can refer to companion members without any prefix at all, because the companion is part of the class’s own scope.
  • Because it is one singleton shared by the whole class, every var declared in a companion object is class-wide mutable state — changing it from one instance is visible to every other instance immediately.

Common Mistakes

Mistake 1: putting const val directly in a class body

class Config {
    const val MAX_SIZE = 100
}

This fails to compile: const is only legal at the top level of a file or as a member of an object/companion object, because a compile-time constant must be resolvable without creating any instance, and a plain class member always belongs to some instance. Move it into a companion object:

class Config {
    companion object {
        const val MAX_SIZE = 100
    }
}

fun main() {
    println("Max size is ${Config.MAX_SIZE}")
}

Output:

Max size is 100

Mistake 2: assuming companion state is per-instance

class Counter {
    companion object {
        var count = 0
    }

    init {
        count++
    }

    fun show() = println("This counter's count: $count")
}

fun main() {
    val a = Counter()
    val b = Counter()
    a.show()
    b.show()
}

Output:

This counter's count: 2
This counter's count: 2

Both objects print 2 because count lives in the single shared companion object, not in each Counter. A beginner expecting “each counter tracks its own count” has actually built a shared, global tally. If the goal really was a per-instance id, keep the running total in the companion but store each instance’s own snapshot in an instance property:

class Counter {
    companion object {
        private var total = 0
    }

    val id: Int = ++total

    fun show() = println("This counter's id: $id")
}

fun main() {
    val a = Counter()
    val b = Counter()
    a.show()
    b.show()
}

Output:

This counter's id: 1
This counter's id: 2

Mistake 3: declaring more than one companion object

class Broken {
    companion object First {
        val a = 1
    }

    companion object Second {
        val b = 2
    }
}
// Compile error: only one companion object is allowed per class

A class may have at most one companion object, named or not. If you need to group unrelated constants and factory functions, use plain nested object declarations instead of a second companion — only the true “class-level, reached via the class name” members belong in the companion.

Best Practices

  • Prefer a companion object over a top-level function when the value is conceptually tied to the class, such as a factory method (create, of, from) or a constant that belongs with the type — otherwise, a plain top-level function is simpler and doesn’t need a class-name prefix.
  • Use const val for genuinely compile-time-known primitives and Strings in a companion object instead of a regular val, since const avoids a runtime property-access call entirely.
  • Reach for a private constructor plus a companion factory function whenever construction needs validation, id generation, or the possibility of returning a cached/existing instance instead of always building a new one.
  • Add @JvmStatic to companion members only when Java callers genuinely need to call them without going through Companion — it’s an interop detail with no effect on Kotlin-only code.
  • Give the companion a name (companion object Factory : ...) when you want to hand it around as a first-class value implementing an interface; leave it unnamed when it’s purely an implementation detail reached only via ClassName.member.
  • Remember a companion object’s var properties are shared, mutable, class-wide state — treat them with the same care you’d give any other shared mutable state, including thread-safety if multiple threads can touch them.

Practice Exercises

  • Write a Temperature class with a private constructor storing degrees Celsius, plus companion factory functions fromCelsius(value: Double) and fromFahrenheit(value: Double) that both return a Temperature. Print a Temperature built each way and confirm they agree for an equivalent value (0°C should equal 32°F).
  • Add a companion object to a Product data class that tracks how many Product instances have been created so far, exposed as a read-only Product.createdCount. Create three products and print the count; make sure the counter cannot be modified from outside the class.
  • Define an interface Parser<T> with one function parse(input: String): T. Give a class of your choosing a named companion object that implements Parser for that class, then write a function that accepts any Parser<T> and uses it, passing your class’s companion in.

Summary

  • Kotlin has no static keyword; a companion object is a single, class-scoped singleton that plays that role and more.
  • ClassName.member is compiler sugar for ClassName.Companion.member (or a true JVM static call under @JvmStatic).
  • A companion object can implement interfaces and be passed around as a value — something Java’s static can never do.
  • Companion members can access the class’s private members, enabling the private-constructor-plus-factory pattern.
  • A class may have at most one companion object; const val is only legal at the top level or inside an object/companion object.
  • All companion state is shared across every instance of the class — it is not per-instance storage.