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 declaration —
object Name { ... }at file or class scope. Creates a named singleton, accessed asName.member. - Companion object —
companion 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 expression —
object : 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 staticINSTANCEfield. - 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 toINSTANCE. - 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
Namein 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
varstate 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 objectinstead of a plainobjectfor stateless singletons, particularly ones used as a branch of asealed 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
varcaptured 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
objectnamedIdGeneratorwith a privatevarcounter starting at 100 and a functionnextId(): Intthat 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 functionunitCircle(): Circlethat returns aCirclewith radius1.0. Print the radius of the circle it returns. - Create an object expression implementing
Runnable(its single method isrun()) that prints"Task executed", store it in aval, and call itsrun()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 objectis a singleton nested inside a class, reached through the class name, and is Kotlin’s replacement for Java’sstaticmembers. - 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 objectadds a readabletoString()and structural equality to a stateless singleton, useful forsealed classbranches.- Singletons share state across the entire program, so mutable state inside an
objectshould be minimal and deliberate, not a substitute for per-instance state.
