Kotlin Companion Object
A companion object lives inside a class and acts as that class's static context. Members of a companion object are accessed directly on the class name, without needing to create an object first. Kotlin does not have static keywords like Java — companion objects replace that role.
Companion Object Basics
class Counter {
companion object {
var totalCreated = 0
fun reset() {
totalCreated = 0
}
}
val id: Int = ++totalCreated
init {
println("Counter #$id created")
}
}
fun main() {
val c1 = Counter() // Counter #1 created
val c2 = Counter() // Counter #2 created
val c3 = Counter() // Counter #3 created
println("Total created: ${Counter.totalCreated}") // 3
Counter.reset()
println("After reset: ${Counter.totalCreated}") // 0
}Companion as Factory
The most common use of companion objects is to provide factory methods — functions that create instances in a controlled way:
class Token private constructor(val value: String, val expiresAt: Long) {
companion object {
fun create(userId: String): Token {
val value = "${userId}_${System.currentTimeMillis()}"
val expiresAt = System.currentTimeMillis() + 3_600_000 // 1 hour
return Token(value, expiresAt)
}
fun expired(): Token {
return Token("expired_token", 0L)
}
}
val isValid: Boolean
get() = System.currentTimeMillis() < expiresAt
}
fun main() {
val t = Token.create("user_42")
println("Token: ${t.value}")
println("Valid: ${t.isValid}")
}Companion Object Diagram
class Database {
companion object { ← shared, class-level
val MAX_POOL_SIZE = 10
fun connect(url: String) { }
}
val connectionId: Int = ... ← per-instance
fun query(sql: String) { } ← per-instance
}
Access:
Database.MAX_POOL_SIZE ← no object needed (companion)
Database.connect("url") ← no object needed (companion)
val db = Database()
db.connectionId ← needs an object (instance member)
db.query("SELECT 1") ← needs an object (instance member)
Named Companion Object
class Color(val r: Int, val g: Int, val b: Int) {
companion object Presets {
val RED = Color(255, 0, 0)
val GREEN = Color(0, 255, 0)
val BLUE = Color(0, 0, 255)
val WHITE = Color(255, 255, 255)
val BLACK = Color(0, 0, 0)
}
override fun toString() = "rgb($r,$g,$b)"
}
fun main() {
println(Color.RED) // rgb(255,0,0)
println(Color.BLUE) // rgb(0,0,255)
println(Color.Presets.GREEN) // can also use the name
}Implementing an Interface
interface Serializable {
fun fromJson(json: String): Any
}
class Config(val host: String, val port: Int) {
companion object : Serializable {
override fun fromJson(json: String): Config {
// simplified parsing
return Config("localhost", 8080)
}
}
}
fun main() {
val config = Config.fromJson("{\"host\":\"localhost\",\"port\":8080}")
println("${config.host}:${config.port}")
}Practical Example: User Manager
class User private constructor(val id: Int, val name: String, val role: String) {
companion object {
private var nextId = 1
private val registry = mutableMapOf()
fun create(name: String, role: String = "member"): User {
val user = User(nextId++, name, role)
registry[user.id] = user
return user
}
fun find(id: Int): User? = registry[id]
fun count(): Int = registry.size
fun all(): List = registry.values.toList()
}
override fun toString() = "User(id=$id, name=$name, role=$role)"
}
fun main() {
User.create("Alice", "admin")
User.create("Bob")
User.create("Carol", "editor")
println("Total users: ${User.count()}") // 3
println(User.find(2)) // User(id=2, name=Bob, role=member)
User.all().forEach { println(it) }
} 