Kotlin Companion Objects

A brief look at the trade offs of Kotlin’s objects

October 18, 2020

In this post I invite you to join me in having a look at Kotlin’s concept of objects. We’ll explore the trade-off of making something an object vs a companion object.

Object’s in Kotlin are singletons, allocated objects which there is only ever a single instance. This is true of named objects, anonymous objects, and companion objects.

object Rick { }  // Named Object
object { } // Annonymous Object.
class SpaceCruiser {
    companion object { } // Companion Object. 
}

Even though these object are all singletons, they differ in their lifetimes. The lifetime defined by the time they are initialized and the time they are de-allocated, or cleaned up.

Named Objects

Named objects are global in scope. They can be referenced by importing them like a typical class. Their initialization occurs at the time of first usage. An instance is created when an element(a variable, function, or value) within the named object, or the object itself is referenced. Subsequent calls to elements contained with in reuse the instance. For practical purposed, consider named objects to be lazy, instantiation deferred to first use.

Anonymous Objects

Anonymous objects are typically declared with the purpose of extending or implementing a class or interface. They are often passed as parameters to functions. The object is allocated at the declaration site, and they live as long as there is a reference to the allocated object, this could be tied to the lifetime of another class, or function. Anonymous object incur an allocation cost when the expression or statement they are contained in is evaluated. This means that allocation of an anonymous object can be deferred via the lazy delegate, but is eagerly evaluated by default.

Companion Objects

Companion objects are a special case of named objects. Classes can only have one companion object. This allows the companion object to be accessed using the class name, which appear similar to static elements in Java. However these elements are not true static properties of the class, a @JvmStatic annotation is required for true static inter-operation with Java code bases. Companion objects are allocated at class load time. This effectively makes companion objects eagerly allocated, and makes companion objects have similar load semantics that similar Java static properties would.

Illustrated Code Example

The code example below is aimed at helping to highlight the properties described above. Objects Rick and Morty are named objects allocated at the time they are first referenced. makeAnonymousInstance is a function that defers the allocation of an object until the function is called. Every invocation of the function returns a newly allocated object. Lastly, SpaceCruiser is a class with a companion object. This companion object is never referenced, however when the class SpaceCruiser is loaded, at first use, the companion object is allocated even though there is no reference to the companion object. Note that SpaceCruiser contains a named object Hyperdrive that remains un-initialized, since there is no reference to it.

object Rick { 
  init { println("Allocating Named Object: Rick") }
}

object Morty { 
  init { println("Allocating Named Object: Morty") }
}

fun makeAnonymousInstance() = object { 
  init { println("Allocating Anonymous Object!") }
}

class SpaceCruiser {
  companion object {
    init { println("Allocating SpaceCruiser Companion Object.") }
  }
  object Hyperdrive { 
    init { println("Allocating SpceCruiser Hyperdrive object") }
  }
}

fun main(args: Array<String>) {
  println("Program Start.")
  makeAnonymousInstance() // Anonymous object created...
  makeAnonymousInstance() // Anonymous object created...again
  val rick = Rick // Named object is allocated since this is first use of 
object.
  // Notice that `Morty` is not allocated, because it is not referenced.
  val ship = SpaceCruiser() // Companion object is allocated, even if not used.
  // Notice that Hyperdrive is not allocated since it is not refeferenced.
}

Takeaways

Kotlin’s objects are quite flexible. In the case of named and companion objects they can help make code more readable. Providing a sort of namespace for collections of constants and function. When considering whether to use a named or companion object there is one rule of thumb I like to follow.

If you’d like to namespace a value or function under an existing Kotlin class, use a companion object, otherwise use a named object. A named object will yield a cleaner syntax in general.

However, since objects lead to allocations of objects, if performance is a concern, consider standalone functions or extension functions as an alternative. It’s possible they may be a more readable and/or performant solution. Attaching companion objects to a class automatically incurs the cost of one additional allocation, regardless of if the companion object is used.