Kotlin: Basics of Companion Objects
If you’ve spent any time writing or reading Kotlin code, then chances are that you’ve come across the somewhat puzzling companion
keyword, and you’ll always see it in conjunction with the object
keyword. So..what does this keyword do?
In short, companion objects are singleton objects whose properties and functions are tied to a class but not to the instance of that class — basically like the “static” keyword in Java but with a twist.
Let’s look at some simple examples:
class Cafe {
companion object {
const val LATTE = "latte"
} fun bestBeverage() = LATTE
}
The following three expressions all return “latte”:
Cafe.Companion.LATTE
Cafe.LATTE
Cafe().bestBeverage()
Notice a few things:
- Methods of the
Cafe
class can reference any property inside the companion object as if that property were declared as a field in theCafe
class itself. - Properties in the companion object can be referenced “statically” as
ContainingClass.PROPERTY_IN_COMPANNION_OBJECT
. - The default name for a companion object is
Companion
.
What if we wanted to give the companion object a name like Beverage
?
class Cafe {
companion object Beverage {
const val LATTE = "latte"
} fun bestBeverage() = LATTE
}// this no longer works
Cafe.Companion.LATTE
Now:
// all return "latte"
Cafe.Beverage.LATTE
Cafe.LATTE
Cafe().bestBeverage()// this no longer compiles
Cafe.Companion.LATTE
Notice that in the first example, since we didn’t give an explicit name to the companion object, a default name of Companion
was assigned.
Okay let’s change the example a little to make LATTE
a private value:
class Cafe {
companion object Beverage {
private const val LATTE = "latte"
} fun bestBeverage() = LATTE
}
Now:
// both of these will give you an error
Cafe.Beverage.LATTE
Cafe.LATTE// this still evaluates to "latte"
Cafe().bestBeverage()
What happens if we want more than 1 companion object?
class Cafe {
companion object Beverage {
const val LATTE = "latte"
} // ERROR: only one companion object is allowed per class
companion object Other {}
}
That’s a no-go.
What about making our companion object implement an interface?
interface Drinkable {
fun drink(): String
val LATTE: String
}class Cafe {
companion object Beverage : Drinkable {
override fun drink() = "i like coffee"
override val LATTE = "latte"
}
}
And we can access the drink
method just like we can access other properties in the companion object:
Cafe.drink()
Okay, here’s the big question: why use companion objects rather than regular objects? Great question, I’m so glad you asked! Here is our working example without using a companion object:
class Cafe {
object Beverage {
const val LATTE = "latte"
} fun bestBeverage() = Beverage.LATTE
}
There are three key differences to notice:
- Unlike companion objects that get assigned a default name of
Companion
, regular object declarations must be named. - Referencing the fields inside a regular object requires the containing class to explicitly use the object name. With companion objects, I could simply write
Cafe.LATTE
, but with regular objects, I have to writeCafe.Beverage.LATTE
. - Companion objects are initialized the first time the containing class is loaded — even if the companion object is not used. As a result, companion objects are not lazy. Regular objects, like the one in the above example, are initialized lazily the first time they are accessed.
So why use companion objects? Because they provide a convenient shorthand for accessing “static” properties/functions. That’s it. If, for some reason, you absolutely must have lazy initialization, then just use a regular object instead.