Read on to learn about companion objects in Kotlin, how they are often misunderstood to replace theΒ static
Β keyword, and non-trivial examples of how to use them to your advantage.
In Kotlin, aΒ companion object
Β is a specialΒ object
Β which is bound to (i.e. is a part of) a class definition. It can be accessed using the name of the enclosing class.
//sampleStart class Thing { init { numThings++ } companion object { var numThings = 0 private set } } fun main() { Thing() Thing() Thing.numThings == 2 // true } //sampleEnd
Companion objects are routinely presented as an alternative to static
, and are regarded as a very controversial design decision. This is unfortunate, because I do not believe this comparison captures the essence of what companion objects are. What they actually are is a powerful upgrade to static nested classes, which incidentally, in combination with other features in Kotlin, make the static
keyword unnecessary and obsolete.
InΒ termsΒ of functionality,Β they are equivalent to singleton static nested classes, with some syntax sugar on top.Β To unpack that beauty of a sentence: their type is not bound to an instance of the outer class, but to the outer class itself (so itβs notΒ Outer().Nested()
Β butΒ Outer.Nested()
), they can see and work withΒ private
Β members of theΒ Outer
Β class, they can inherit from superclasses, they implement interfaces, we can pass instances of the nested class around, and additionally:
- they are singletons, so itβs not even
Outer.Nested()
, it's justOuter.Nested
- Kotlin adds syntax sugar that allows us to access the singleton without an additional class qualifier, so itβs not even
Outer.Nested
, it's justOuter
If you do need to reference theΒ typeΒ of theΒ companion object
, useΒ Outer.Companion
.
Static keyword usage
Letβs go through a few use cases of the static
keyword, and show how they are covered by existing features in Kotlin.
Stateless properties/functions (i.e. not bound to a specific class instance)
In Java, everything has to be in a class, even things that donβt belong in one. Typically, utility functions fall into this category, and (being utility functions, which are usually stateless) are usually declared static
.
In Kotlin, you have two choices:
- Is the function/property stateless and has no conceptual connection to any single type (or maybe more than one)? Define a top-level function/property
- Is the function/variable stateless, but is conceptually connected to a class instance? Define it inside the
companion object
This helps clean up, and avoid, bloated classes, but would not work if Kotlin didnβt have top-level declarations. It is the synergy of these two features that make static
unnecessary.
Properties/functions needing access to private members of a class
A typical use case is wanting a class to be constructed in a particular way, e.g. checking a cache first. In such situations, the class has a private constructor, and the construction is done by a static
method / static nested class (i.e., a builder). Both approaches have downsides.
The static
approach doesn't allow to cleanly separate builder code from core code, and makes it difficult to pass it around (we usually have to wrap it in anonymous objects which implement some sort of Builder
interface). We also have no way of sharing code.
//sampleStart // Java interface Builder<T> { T build(); } class ComplexObject { private ComplexObject() { } // How do we make this conform to Builder<ComplexObject>? public static ComplexObject build() { // Check cache etc. return new ComplexObject(); } } class LazyContainer<T> { private final Builder<T> builder; boolean initialized = false; private T value = null; public LazyContainer(Builder<T> builder) { this.builder = builder; } public T getValue() { if(!initialized) { value = builder.build(); initialized = true; } return value; } } class Main { public static void main(String[] args) { // What we are actually doing here is passing // a lambda that is converted to an anonymous // object implementing Builder<ComplexObject> new LazyContainer<>(ComplexObject::build); } } //sampleEnd
The static nested class approach solves the above by allowing the code to be encapsulated inside the static nested class, which can implement an interface. The downside is having to specify the static nested classes name whenever we access the members, and having to create an instance of it.
//sampleStart interface Builder<T> { T build(); } class ComplexObject { private ComplexObject() { } static class NestedBuilder implements Builder<ComplexObject> { @Override public ComplexObject build() { // Check cache etc. return new ComplexObject(); } } } class LazyContainer<T> { private final Builder<T> builder; boolean initialized = false; private T value = null; public LazyContainer(Builder<T> builder) { this.builder = builder; } public T getValue() { if(!initialized) { value = builder.build(); initialized = true; } return value; } } class Main { public static void main(String[] args) { new LazyContainer<>(new ComplexObject.NestedBuilder()); } } //sampleEnd
Companion objects solve both of these problems.
//sampleStart interface Builder<T> { fun build(): T } class ComplexObject private constructor() { companion object : Builder<ComplexObject> { override fun build(): ComplexObject { // Check cache etc. return ComplexObject() } } } class LazyContainer<T>(private val builder: Builder<T>) { // The 'by lazy' defines a delegated property, which // we'll talk about in a future article. It is the // idiomatic way laziness is handled in Kotlin, // and does the same thing as the Java version above val value: T by lazy { builder.build() } } fun main() { // No need to specify anything LazyContainer(ComplexObject) } //sampleEnd
This approach also allows for abstracting of common functionality and distributing it through inheritance, which is what you will be implementing in the second exercise bellow.
You can skim through the docs to find out about some nuances concerning companion objects.
You can also read here and here to see different examples of companion object usage, although they are similar to what we talked about.
Exercises
This exercise deals with fetching some records (represented by the RecordOrig
and RecordImpl
classes) from a storage, and caching the result.
Here are the building blocks which youβll be using,Β alongΒ with a first implementation:
import org.junit.Assert import org.junit.Test class Test { @Test fun testRecordOrig() { val fetcher = RecordOrig.Fetcher() val recordOrig = fetcher.fetch(123) val recordImpl = fetcher.fetch(123) Assert.assertTrue("Check implementation of RecordOrig", recordOrig.equals(recordImpl)) } } //sampleStart interface Record fun retrieveValueFromStorage(id: Int): String { // Lengthy operation return Math.random().toString(); } interface RecordFetcher<I, R : Record> { fun fetch(id: I): R } data class RecordOrig private constructor(val id: Int, val value: String) : Record { class Fetcher : RecordFetcher<Int, RecordOrig> { private val cache: MutableMap<Int, RecordOrig> = mutableMapOf() override fun fetch(id: Int): RecordOrig = cache.getOrPut(id) { RecordOrig(id, retrieveValueFromStorage(id)) } } } //sampleEnd
Exercise 1
Implement RecordImpl
in the same spirit as RecordOrig
, but use a companion object instead.
import org.junit.Assert import org.junit.Test class Test { @Test fun testRecordImpl() { val recordOrig = RecordImpl.fetch(123) val recordImpl = RecordImpl.fetch(123) Assert.assertTrue("Check implementation of RecordImpl", recordOrig.equals(recordImpl)) } } //sampleStart interface Record fun retrieveValueFromStorage(id: Int): String { // Lengthy operation return Math.random().toString(); } interface RecordFetcher<I, R : Record> { fun fetch(id: I): R } /** * Implement RecordImpl in the same spirit as RecordOrig, but use a companion * object instead of an inner class. */ data class RecordImpl private constructor(val id: Int, val value: String) : Record //sampleEnd
Exercise 2
ImplementΒ RecordImpl
Β again using a companion object, but this time take advantage ofΒ MutableMapCache
Β to abstract away the details of the cache.
import org.junit.Assert import org.junit.Test class Test { @Test fun testRecordImpl() { val recordOrig = RecordImpl.fetch(123) val recordImpl = RecordImpl.fetch(123) Assert.assertTrue("Check implementation of RecordImpl", recordOrig.equals(recordImpl)) } } //sampleStart interface Record fun retrieveValueFromStorage(id: Int): String { // Lengthy operation return Math.random().toString(); } interface RecordFetcher<I, R : Record> { fun fetch(id: I): R } abstract class MutableMapCache<K, O> { protected val cache: MutableMap<K, O> = mutableMapOf() } /** * Implement RecordImpl by taking advantage of MutableMapCache to abstract away * the details of the cache. */ data class RecordImpl private constructor(val id: Int, val value: String) : Record //sampleEnd