article banner

Objects in Kotlin

This is a chapter from the book Kotlin Essentials. You can find it on LeanPub or Amazon. It is also available as a course.

What is an object? This is the question I often start this section with in my workshops, and I generally get an instant response, "An instance of a class". That is right, but how do we create objects? One way is easy: using constructors.

class A // Using a constructor to create an object val a = A()

However, this is not the only way. In Kotlin, we can also create objects using object expression and object declaration. Let's discuss these two options.

Object expressions

To create an empty object using an expression, we use the object keyword and braces. This syntax for creating objects is known as object expression.

val instance = object {}

An empty object extends no classes (except for Any, which is extended by all objects in Kotlin), implements no interfaces, and has nothing inside its body. Nevertheless, it is useful. Its power lies in its uniqueness: such an object equals nothing else but itself. Therefore, it is perfectly suited to be used as some kind of token or synchronization lock.

class Box { var value: Any? = NOT_SET fun initialized() = value != NOT_SET companion object { private val NOT_SET = object {} } } private val LOCK = object {} fun synchronizedOperation() = synchronized(LOCK) { // ... }

An empty object can also be created with the constructor of Any, so Any() is an alternative to object {}.

private val NOT_SET = Any()

However, objects created with an object expression do not need to be empty. They can have bodies, extend classes, implement interfaces, etc. The syntax is the same as for classes, but object declarations use the object keyword instead of class and should not define the name or constructor.

data class User(val name: String) interface UserProducer { fun produce(): User } fun printUser(producer: UserProducer) { println(producer.produce()) } fun main() { val user = User("Jake") val producer = object : UserProducer { override fun produce(): User = user } printUser(producer) // User(name=Jake) }

In a local scope, object expressions define an anonymous type that won’t work outside the class where it is defined. This means the non-inherited members of object expressions are accessible only when an anonymous object is declared in a local or class-private scope; otherwise, the object is just an opaque Any type, or the type of the class or interface it inherits from. This makes non-inherited members of object expressions hard to use in real-life projects.

class Robot { // Possible, but rarely useful // prefer regular member properties instead private val point = object { var x = 0 var y = 0 } fun moveUp() { point.y += 10 } fun show() { println("(${point.x}, ${point.y})") } } fun main() { val robot = Robot() robot.show() // (0, 0) robot.moveUp() robot.show() // (0, 10) val point = object { var x = 0 var y = 0 } println(point.x) // 0 point.y = 10 println(point.y) // 10 }

In practice, object expressions are used as an alternative to Java anonymous classes, i.e., when we need to create a watcher or a listener with multiple handler methods.

taskNameView.addTextChangedListener(object : TextWatcher { override fun afterTextChanged( editable: Editable? ) { //... } override fun beforeTextChanged( text: CharSequence?, start: Int, count: Int, after: Int ) { //... } override fun onTextChanged( text: CharSequence?, start: Int, before: Int, count: Int ) { //... } })

Note that "object expression" is a better name than "anonymous class" since this is an expression that produces an object.

Object declaration

If we take an object expression and give it a name, we get an object declaration. This structure also creates a single object, but this object is not anonymous: it has a name that can be used to reference it.

object Point { var x = 0 var y = 0 } fun main() { println(Point.x) // 0 Point.y = 10 println(Point.y) // 10 val p = Point p.x = 20 println(Point.x) // 20 println(Point.y) // 10 }

Object declaration is an implementation of a singleton pattern4, so this declaration creates a class with a single instance. Whenever we want to use this class, we need to operate on this single instance. Object declarations support all the features that classes support; for example, they can extend classes or implement interfaces.

data class User(val name: String) interface UserProducer { fun produce(): User } object FakeUserProducer : UserProducer { override fun produce(): User = User("fake") } fun setUserProducer(producer: UserProducer) { println(producer.produce()) } fun main() { setUserProducer(FakeUserProducer) // User(name=fake) }

Companion objects

When I reflect on the times when I worked as a Java developer, I remember discussions about what features should be introduced into that language. A common idea I often heard was introducing inheritance for static elements. In the end, inheritance is very important in Java, so why can't we use it for static elements? Kotlin has addressed this problem with companion objects; however, to make that possible, it first needed to eliminate actual static elements, i.e., elements that are called on classes, not on objects.

// Java
class User {
   // Static element definition
   public static User empty() {
       return new User();
   }
}

// Static element usage
User user = User.empty()

Yes, we don’t have static elements in Kotlin, but we don’t need them because we use object declarations instead. If we define an object declaration in a class, it is static by default (just like classes defined inside classes), so we can directly call its elements.

// Kotlin class User { object Producer { fun empty() = User() } } // Usage val user: User = User.Producer.empty()

This is not as convenient as static elements, but we can improve it. If we use the companion keyword before an object declaration defined inside a class, then we can call these object methods implicitly "on the class".

class User { companion object Producer { fun empty() = User() } } // Usage val user: User = User.empty() // or val user: User = User.Producer.empty()

Objects with the companion modifier, also known as companion objects, do not need an explicit name. Their default name is Companion.

class User { companion object { fun empty() = User() } } // Usage val user: User = User.empty() // or val user: User = User.Companion.empty()

This is how we achieved a syntax that is nearly as convenient as static elements. The only inconvenience is that we must locate all the “static” elements inside a single object (there can be only one companion object in a class). This is a limitation, but we have something in return: companion objects are objects, so they can extend classes or implement interfaces.

Let me show you an example. Let’s say that you represent money in different currencies using different classes like USD, EUR, or PLN. For convenience, each of these defines from builder functions, which simplify object creation.

import java.math.BigDecimal import java.math.MathContext import java.math.RoundingMode.HALF_EVEN abstract class Money( val amount: BigDecimal, val currency: String ) class USD(amount: BigDecimal) : Money(amount, "USD") { companion object { private val MATH = MathContext(2, HALF_EVEN) fun from(amount: Int): USD = USD(amount.toBigDecimal(MATH)) fun from(amount: Double): USD = USD(amount.toBigDecimal(MATH)) fun from(amount: String): USD = USD(amount.toBigDecimal(MATH)) } } class EUR(amount: BigDecimal) : Money(amount, "EUR") { companion object { private val MATH = MathContext(2, HALF_EVEN) fun from(amount: Int): EUR = EUR(amount.toBigDecimal(MATH)) fun from(amount: Double): EUR = EUR(amount.toBigDecimal(MATH)) fun from(amount: String): EUR = EUR(amount.toBigDecimal(MATH)) } } class PLN(amount: BigDecimal) : Money(amount, "PLN") { companion object { private val MATH = MathContext(2, HALF_EVEN) fun from(amount: Int): PLN = PLN(amount.toBigDecimal(MATH)) fun from(amount: Double): PLN = PLN(amount.toBigDecimal(MATH)) fun from(amount: String): PLN = PLN(amount.toBigDecimal(MATH)) } } fun main() { val eur: EUR = EUR.from("12.00") val pln: PLN = PLN.from(20) val usd: USD = USD.from(32.5) }

The repetitive functions for creating objects from different types can be extracted into an abstract MoneyMaker class, which can be extended by companion objects of different currencies. This class can offer a range of methods to create a currency. This way, we use companion object inheritance to extract a pattern that is common to all companion objects of classes that represent money.

import java.math.BigDecimal import java.math.MathContext import java.math.RoundingMode.HALF_EVEN abstract class Money( val amount: BigDecimal, val currency: String ) abstract class MoneyMaker<M : Money> { private val MATH = MathContext(2, HALF_EVEN) abstract fun from(amount: BigDecimal): M fun from(amount: Int): M = from(amount.toBigDecimal(MATH)) fun from(amount: Double): M = from(amount.toBigDecimal(MATH)) fun from(amount: String): M = from(amount.toBigDecimal(MATH)) } class USD(amount: BigDecimal) : Money(amount, "USD") { companion object : MoneyMaker<USD>() { override fun from(amount: BigDecimal): USD = USD(amount) } } class EUR(amount: BigDecimal) : Money(amount, "EUR") { companion object : MoneyMaker<EUR>() { override fun from(amount: BigDecimal): EUR = EUR(amount) } } class PLN(amount: BigDecimal) : Money(amount, "PLN") { companion object : MoneyMaker<PLN>() { override fun from(amount: BigDecimal): PLN = PLN(amount) } } fun main() { val eur: EUR = EUR.from("12.00") val pln: PLN = PLN.from(20) val usd: USD = USD.from(32.5) }

Our community is still learning how to use these capabilities, but you can already find plenty of examples in projects and libraries. Here are a few interesting examples6:

// Using companion object inheritance for logging // from the Kotlin Logging framework class FooWithLogging { fun bar(item: Item) { logger.info { "Item $item" } // Logger comes from the companion object } companion object : KLogging() // companion inherits logger property }
// Android-specific example of using an abstract factory // for companion object class MainActivity : Activity() { //... // Using companion object as a factory companion object : ActivityFactory() { override fun getIntent(context: Context): Intent = Intent(context, MainActivity::class.java) } } abstract class ActivityFactory { abstract fun getIntent(context: Context): Intent fun start(context: Context) { val intent = getIntent(context) context.startActivity(intent) } fun startForResult(activity: Activity, requestCode: Int) { val intent = getIntent(activity) activity.startActivityForResult(intent, requestCode) } }
// Usage of all the members of the companion ActivityFactory val intent = MainActivity.getIntent(context) MainActivity.start(context) MainActivity.startForResult(activity, requestCode) // In contexts on Kotlin Coroutines, companion objects are // used as keys to identify contexts data class CoroutineName( val name: String ) : AbstractCoroutineContextElement(CoroutineName) { // Companion object is a key companion object Key : CoroutineContext.Key<CoroutineName> override fun toString(): String = "CoroutineName($name)" } // Finding a context by key val name1 = context[CoroutineName] // Yes, this is a companion // You can also refer to companion objects by its name val name2 = context[CoroutineName.Key]

Data object declarations

Since Kotlin 1.8, you can use the data modifier for object declarations. It generates the toString method for the object; this method includes the object name as a string.

data object ABC fun main() { println(ABC) // ABC }

Constant values

It’s common practice to generally extract constant values as properties of companion objects and name them using UPPER_SNAKE_CASE5. This way, we name those values and simplify their changes in the future. We name constant values in a characteristic way to make it clear that they represent a constant2.

class Product( val code: String, val price: Double, ) { init { require(price > MIN_AMOUNT) } companion object { val MIN_AMOUNT = 5.00 } }

When companion object properties or top-level properties represent a constant value (known at compile time) that is either a primitive or a String3, we can add the const modifier. This is an optimization. All usages of such variables will be replaced with their values at compile time.

class Product( val code: String, val price: Double, ) { init { require(price > MIN_AMOUNT) } companion object { const val MIN_AMOUNT = 5.00 } }

Such properties can also be used in annotations:

private const val OUTDATED_API: String = "This is a part of an outdated API." @Deprecated(OUTDATED_API) fun foo() { ... } @Deprecated(OUTDATED_API) fun boo() { ... }

Summary

In this chapter, we've learned that objects can be created not only from classes but also using object expressions and object declarations. Both these kinds of objects have practical usages. Object expression is used as an alternative to Java anonymous objects, but it offers more. Object declaration is Kotlin's implementation of the singleton pattern. A special form of object declaration, known as a companion object, is used as an alternative to static elements but with additional support for inheritance. We also have the const modifier, which offers better support for constant elements defined at the top level or in object declarations.

In the previous chapter, we discussed data classes, but there are other modifiers we use for classes in Kotlin. In the next chapter, we will learn about another important type of class: exceptions.

2:

This practice is better described in Effective Kotlin, Item 26: Use abstraction to protect code against changes.

3:

So, the accepted types are Int, Long, Double, Float, Short, Byte, Boolean, Char, and String.

4:

A programming pattern where a class is implemented such that it can have only one instance.

5:

UPPER_SNAKE_CASE is a naming convention where each character is capitalized, and we separate words with an underscore, like in the UPPER_SNAKE_CASE name. Using it for constants is suggested in the Kotlin documentation in the section Kotlin Coding Convention.

6:

Do not treat them as best practices but rather as examples of what you might do with the fact that companion objects can inherit from classes and implement interfaces.