Kotlin object, companion object, and delegation

Create singletons, class-level factories with private constructors, and reuse behaviour by delegating to an interface.

Kotlin
interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println("LOG: $message")
}

class Service(logger: Logger) : Logger by logger {
    fun run() {
        log("service started")
    }
}

class Counter private constructor(val id: Int) {
    companion object {
        private var next = 1
        fun create(): Counter = Counter(next++)
    }
}

object Config {
    val name = "demo"
    fun describe() = "config $name"
}

fun main() {
    Service(ConsoleLogger()).run()
    println(Counter.create().id)
    println(Counter.create().id)
    println(Config.describe())
}
Output
LOG: service started
1
2
config demo

Explanation

An object declaration creates a singleton: one instance, created on first use, with no need for a static keyword. A companion object holds members that belong to the class itself, such as factory functions. With a private constructor, the only way to build instances is through the factory, which can enforce rules like numbering the instances.

Class delegation, written class Service(logger: Logger) : Logger by logger, makes Service implement Logger by forwarding every call to the object passed in. It reuses behaviour without inheritance and lets the logger be replaced, for example by a fake one in tests. It is composition made convenient by the language.

Explore the dry run
Guided walkthroughReady
1interface Logger {
2 fun log(message: String)
3}
4
5class ConsoleLogger : Logger {
6 override fun log(message: String) = println("LOG: $message")
7}
8
9class Service(logger: Logger) : Logger by logger {
10 fun run() {
11 log("service started")
12 }
13}
14
15class Counter private constructor(val id: Int) {
16 companion object {
17 private var next = 1
18 fun create(): Counter = Counter(next++)
19 }
20}
21
22object Config {
23 val name = "demo"
24 fun describe() = "config $name"
25}
26
27fun main() {
28 Service(ConsoleLogger()).run()
29 println(Counter.create().id)
30 println(Counter.create().id)
31 println(Config.describe())
32}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Service implements Logger by delegating every method to the given logger.—
—
log is called on Service itself, but the work is done by the ConsoleLogger it was given.—
—
The delegated call prints the message.—
LOG: service started
Nobody outside the class can call the constructor.—
LOG: service started
The companion's factory numbers each new counter.—
LOG: service started
The first counter gets id 1.—
LOG: service started
1
The second counter gets id 2.—
LOG: service started
1
2
An object is used directly by name, like a single shared instance.—
LOG: service started
1
2
config demo

Cost and common mistakes

O(1) per call.

  • The companion object's state is shared by all instances and is not thread-safe. Guard it if several threads create counters.
  • An object is created lazily and lives for the whole program. Avoid putting mutable global state in it.
  • Delegation forwards only the interface's methods. Other members of the delegate are not exposed.

Try it yourself

How would you test Service without printing?

Reveal answer and reasoning

Pass a fake Logger that stores messages in a list, then check the list. That is possible because Service depends on the Logger interface.

Further reading