The object Keyword and Singletons

Kotlin has a single keyword, object, that covers three different jobs Java needs separate boilerplate for: creating a thread-safe singleton, attaching class-level members without a static keyword, and creating a one-off anonymous instance of an interface or class. Understanding object means understanding how Kotlin eliminated the classic public static final singleton pattern and Java’s static keyword entirely, replacing both with ordinary, well-typed object-oriented syntax.

Overview / How It Works

In plain Kotlin, every class is a blueprint you instantiate with a constructor, possibly many times. An object is different: it declares a class and creates exactly one instance of it, at the same time. You never write MySingleton() — there is no constructor to call because there is only ever one instance, created lazily the first time it is referenced and reused for the lifetime of the program.

Under the hood, the Kotlin compiler generates a regular JVM class with a private constructor and a public static field (traditionally named INSTANCE) that holds the single instance, initialized in a static initializer block. Static initializers on the JVM are guaranteed by the class-loading mechanism to run exactly once and to be visible to all threads before any thread can observe the field — so a Kotlin object is a thread-safe singleton with no synchronization code required from you. This is the single biggest advantage over hand-writing a singleton in Java, where getting double-checked locking or eager-vs-lazy initialization right is a well-known source of bugs.

object shows up in three related but distinct forms:

  • Object declarationobject Name { ... } at file or class scope. Creates a named singleton, accessed as Name.member.
  • Companion objectcompanion object { ... } declared inside a class. A singleton tied to that class, used for factory functions and constants, and accessed through the class name itself (ClassName.member) rather than through an instance.
  • Object expressionobject : SomeType { ... } used as an expression. Creates an anonymous, unnamed instance on the spot — Kotlin’s replacement for Java’s anonymous inner classes.

Kotlin 2.x also has data object, a variant of the object declaration that behaves like a lightweight data class for objects that carry no state: it auto-generates a readable toString() (the object’s simple name) and a structural equals()/hashCode(), which matters when the object is used as one branch of a sealed class hierarchy and gets compared or printed.

Syntax

object Name {
    // properties, functions, initializer blocks
}

class Outer {
    companion object {
        // members reachable as Outer.member
    }
}

val instance = object : SomeInterface {
    // anonymous implementation
}
Form Named? Access Typical use
Object declaration Yes Name.member Global singleton: config, registry, utility namespace
Companion object Optional (default name Companion) ClassName.member Factory functions, constants tied to a class
Object expression No Local variable it’s assigned to One-off interface implementation (like a Java anonymous class)

Examples

Example 1: A singleton object declaration

object Counter {
    var count = 0
        private set

    fun increment() {
        count++
    }
}

fun main() {
    Counter.increment()
    Counter.increment()
    Counter.increment()
    println("Count: ${Counter.count}")
}

Output:

Count: 3

Counter is never constructed with Counter() — there is exactly one Counter for the whole program, so every call to Counter.increment() mutates the same count. The private set makes count readable from anywhere but only mutable from inside Counter itself, a common pattern for exposing controlled state from a singleton.

Example 2: A companion object as a factory

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 primary constructor of User is private, so the only way to build a User from outside the class is through User.create(...). This is the standard Kotlin replacement for Java’s static factory methods: the companion object lives inside the class body, can see its private members (including the private constructor), and is reached through the class name exactly like a Java static method call — but it is a real singleton object, not a bag of static functions.

Example 3: An object expression (anonymous object)

fun main() {
    val names = mutableListOf("Charlie", "alice", "Bob")

    val comparator = object : Comparator<String> {
        override fun compare(a: String, b: String): Int {
            return a.lowercase().compareTo(b.lowercase())
        }
    }

    names.sortWith(comparator)
    println(names)
}

Output:

[alice, Bob, Charlie]

There is no named class here at all — object : Comparator<String> { ... } creates a brand-new, unnamed type that implements Comparator<String> and immediately instantiates it, assigning the single instance to comparator. Unlike an object declaration, a new object expression evaluated a second time produces a distinct instance; it is not a singleton, only an inline, throwaway implementation.

Example 4: data object for a stateless singleton

data object Empty {
    val description = "nothing here"
}

fun main() {
    println(Empty)
    println(Empty == Empty)
}

Output:

Empty
true

data object (Kotlin 1.9+) gives an object declaration a readable toString() that prints its simple name instead of the default Empty@1a2b3c-style hash. This is mainly useful when an object is one case of a sealed class, since printing or logging that case now shows something meaningful.

How It Works Step by Step

  • The compiler sees object Name { ... } and generates a final class with a private constructor and a static INSTANCE field.
  • The first time any code references Name, the JVM class loader runs the class’s static initializer, which builds the one instance and assigns it to INSTANCE.
  • The JVM’s class-loading guarantees make this both lazy (nothing happens until first use) and thread-safe (no two threads can race to create two instances).
  • Every later reference to Name in the program resolves to that same already-built instance — there is no second construction, ever.
  • For a companion object, the same mechanism applies, but the generated singleton is nested inside the outer class and reached through the outer class’s name rather than its own.
  • For an object expression, no singleton is generated at all — a fresh anonymous class is generated at that source location, and a new instance is created every time execution reaches that expression.

Common Mistakes

Mistake 1: Trying to instantiate an object declaration

An object declaration already is the instance. Calling it like a constructor is a compile error, not a runtime bug — Kotlin catches it immediately.

object Logger {
    fun log(message: String) = println(message)
}

fun main() {
    val logger = Logger() // error: object declarations cannot be instantiated
    logger.log("hello")
}

Fix it by referencing the object by name directly — no parentheses, no val needed:

object Logger {
    fun log(message: String) = println(message)
}

fun main() {
    Logger.log("hello")
}

Output:

hello

Mistake 2: Using a singleton where per-instance state was actually needed

Because an object is shared by the entire program, any mutable state it holds is shared too. Modeling something that should be per-user or per-request as a singleton silently mixes unrelated data together:

object ShoppingCart {
    val items = mutableListOf<String>()

    fun addItem(item: String) {
        items.add(item)
    }
}

fun main() {
    ShoppingCart.addItem("Book")
    ShoppingCart.addItem("Pen")
    println(ShoppingCart.items)
}

Output:

[Book, Pen]

This compiles fine, but if the intent was one cart per customer, ShoppingCart is the wrong tool — every customer in the app would share the exact same list. The fix is an ordinary class, instantiated once per user:

class ShoppingCart {
    val items = mutableListOf<String>()

    fun addItem(item: String) {
        items.add(item)
    }
}

fun main() {
    val aliceCart = ShoppingCart()
    aliceCart.addItem("Book")

    val bobCart = ShoppingCart()
    bobCart.addItem("Pen")

    println("Alice: ${aliceCart.items}")
    println("Bob: ${bobCart.items}")
}

Output:

Alice: [Book]
Bob: [Pen]

Reach for object only when there is genuinely one logical instance for the whole application — configuration holders, stateless utility namespaces, and registries are good fits; per-user or per-request data is not.

Best Practices

  • Use an object declaration for genuinely global, stateless-or-shared concepts: configuration, logging utilities, constant registries.
  • Prefer a companion object over a top-level function when the function logically belongs to a class, especially for factory methods that need access to a private constructor.
  • Keep mutable var state inside a singleton to a minimum — every mutation is visible to every caller everywhere in the program, which is exactly the kind of shared mutable state that causes hard-to-reproduce bugs, especially across threads.
  • Use data object instead of a plain object for stateless singletons, particularly ones used as a branch of a sealed class, so logging and debugging show a real name instead of a hash.
  • Reach for an object expression instead of a full named class when you need a one-off implementation of an interface used in exactly one place — it keeps the implementation next to where it is used.
  • Remember object expressions capture variables from their enclosing scope by reference, so a var captured by an object expression can be read and modified by that object’s methods, just like a Java anonymous class capturing an effectively-final variable, but without Kotlin’s restriction to read-only captures.

Practice Exercises

  • Write an object named IdGenerator with a private var counter starting at 100 and a function nextId(): Int that returns the counter and then increments it. Call it three times and print each result.
  • Give a class Circle(val radius: Double) a companion object with a function unitCircle(): Circle that returns a Circle with radius 1.0. Print the radius of the circle it returns.
  • Create an object expression implementing Runnable (its single method is run()) that prints "Task executed", store it in a val, and call its run() method. Expected output: Task executed.

Summary

  • object Name { ... } declares and instantiates a lazy, thread-safe singleton in one step — it is never constructed with parentheses.
  • A companion object is a singleton nested inside a class, reached through the class name, and is Kotlin’s replacement for Java’s static members.
  • An object expression (object : Type { ... }) creates an unnamed, one-off instance and is Kotlin’s replacement for Java’s anonymous inner classes; it is not a singleton.
  • data object adds a readable toString() and structural equality to a stateless singleton, useful for sealed class branches.
  • Singletons share state across the entire program, so mutable state inside an object should be minimal and deliberate, not a substitute for per-instance state.