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.
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.
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.
An empty object can also be created with the constructor of Any
, so Any()
is an alternative to object {}
.
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.
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.
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.
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 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.
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.
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".
Objects with the companion
modifier, also known as companion objects, do not need an explicit name. Their default name is Companion
.
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.
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.
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:
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.
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.
When companion object properties or top-level properties represent a constant value (known at compile time) that is either a primitive or a String
3, we can add the const
modifier. This is an optimization. All usages of such variables will be replaced with their values at compile time.
Such properties can also be used in annotations:
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.
This practice is better described in Effective Kotlin, Item 26: Use abstraction to protect code against changes.
So, the accepted types are Int
, Long
, Double
, Float
, Short
, Byte
, Boolean
, Char
, and String
.
A programming pattern where a class is implemented such that it can have only one instance.
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.
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.