Object declarations and expressions
In Kotlin, objects allow you to define a class and create an instance of it in a single step. This is useful when you need either a reusable singleton instance or a one-time object. To handle these scenarios, Kotlin provides two key approaches: object declarations for creating singletons and object expressions for creating anonymous, one-time objects.
Object declarations and object expressions are best used for scenarios when:
Using singletons for shared resources: You need to ensure that only one instance of a class exists throughout the application. For example, managing a database connection pool.
Creating factory methods: You need a convenient way to create instances efficiently. Companion objects allow you to define class-level functions and properties tied to a class, simplifying the creation and management of these instances.
Modifying existing class behavior temporarily: You want to modify the behavior of an existing class without the need to create a new subclass. For example, adding temporary functionality to an object for a specific operation.
Type-safe design is required: You require one-time implementations of interfaces or abstract classes using object expressions. This can be useful for scenarios like a button click handler.
Object declarations
You can create single instances of objects in Kotlin using object declarations, which always have a name following the object
keyword. This allows you to define a class and create an instance of it in a single step, which is useful for implementing singletons:
To refer to the object
, use its name directly:
Object declarations can also have supertypes, similar to how anonymous objects can inherit from existing classes or implement interfaces:
Like variable declarations, object declarations are not expressions, so they cannot be used on the right-hand side of an assignment statement:
Object declarations cannot be local, which means they cannot be nested directly inside a function. However, they can be nested within other object declarations or non-inner classes.
Data objects
When printing a plain object declaration in Kotlin, the string representation contains both its name and the hash of the object
:
However, by marking an object declaration with the data
modifier, you can instruct the compiler to return the actual name of the object when calling toString()
, the same way it works for data classes:
Additionally, the compiler generates several functions for your data object
:
toString()
returns the name of the data objectequals()
/hashCode()
enables equality checks and hash-based collections
The equals()
function for a data object
ensures that all objects that have the type of your data object
are considered equal. In most cases, you will only have a single instance of your data object
at runtime, since a data object
declares a singleton. However, in the edge case where another object of the same type is generated at runtime (for example, by using platform reflection with java.lang.reflect
or a JVM serialization library that uses this API under the hood), this ensures that the objects are treated as being equal.
The generated hashCode()
function has a behavior that is consistent with the equals()
function, so that all runtime instances of a data object
have the same hash code.
Differences between data objects and data classes
While data object
and data class
declarations are often used together and have some similarities, there are some functions that are not generated for a data object
:
No
copy()
function. Because adata object
declaration is intended to be used as singletons, nocopy()
function is generated. Singletons restrict the instantiation of a class to a single instance, which would be violated by allowing copies of the instance to be created.No
componentN()
function. Unlike adata class
, adata object
does not have any data properties. Since attempting to destructure such an object without data properties wouldn't make sense, nocomponentN()
functions are generated.
Use data objects with sealed hierarchies
Data object declarations are particularly useful for sealed hierarchies like sealed classes or sealed interfaces. They allow you to maintain symmetry with any data classes you may have defined alongside the object.
In this example, declaring EndOfFile
as a data object
instead of a plain object
means that it will get the toString()
function without the need to override it manually:
Companion objects
Companion objects allow you to define class-level functions and properties. This makes it easy to create factory methods, hold constants, and access shared utilities.
An object declaration inside a class can be marked with the companion
keyword:
Members of the companion object
can be called simply by using the class name as the qualifier:
The name of the companion object
can be omitted, in which case the name Companion
is used:
Class members can access private
members of their corresponding companion object
:
When a class name is used by itself, it acts as a reference to the companion object of the class, regardless of whether the companion object is named or not:
Although members of companion objects in Kotlin look like static members from other languages, they are actually instance members of the companion object, meaning they belong to the object itself. This allows companion objects to implement interfaces:
However, on the JVM, you can have members of companion objects generated as real static methods and fields if you use the @JvmStatic
annotation. See the Java interoperability section for more detail.
Object expressions
Object expressions declare a class and create an instance of that class, but without naming either of them. These classes are useful for one-time use. They can either be created from scratch, inherit from existing classes, or implement interfaces. Instances of these classes are also called anonymous objects because they are defined by an expression, not a name.
Create anonymous objects from scratch
Object expressions start with the object
keyword.
If the object doesn't extend any classes or implement interfaces, you can define an object's members directly inside curly braces after the object
keyword:
Inherit anonymous objects from supertypes
To create an anonymous object that inherits from some type (or types), specify this type after object
and a colon :
. Then implement or override the members of this class as if you were inheriting from it:
If a supertype has a constructor, pass the appropriate constructor parameters to it. Multiple supertypes can be specified, separated by commas, after the colon:
Use anonymous objects as return and value types
When you return an anonymous object from a local or private
function or property (but not an inline function), all the members of that anonymous object are accessible through that function or property:
This allows you to return an anonymous object with specific properties, offering a simple way to encapsulate data or behavior without creating a separate class.
If a function or property that returns an anonymous object is public
or private
, its actual type is:
Any
if the anonymous object doesn't have a declared supertype.The declared supertype of the anonymous object, if there is exactly one such type.
The explicitly declared type if there is more than one declared supertype.
In all these cases, members added in the anonymous object are not accessible. Overridden members are accessible if they are declared in the actual type of the function or property. For example:
Access variables from anonymous objects
Code within the body of object expressions can access variables from the enclosing scope:
Behavior difference between object declarations and expressions
There are differences in the initialization behavior between object declarations and object expressions:
Object expressions are executed (and initialized) immediately, where they are used.
Object declarations are initialized lazily, when accessed for the first time.
A companion object is initialized when the corresponding class is loaded (resolved) that matches the semantics of a Java static initializer.