Object creation is a heavy process. When we create a class object, all the public and private properties of that class are initialised inside the constructor. Every variable inside a class initialisation requires a certain amount of time to allocate the memory on the heap and hold its reference on the stack. The more variables, the more time it may take but since the time is in microseconds or even less, it’s not observable.
Sometimes we don’t need all the objects to be initialised during the class object creation itself.
There can be two reasons for that.
- That object/property/variable is dependent on another object to initialise first and use its reference.
- The flow is such that we need that object only in a certain condition.
In Kotlin, we have certain features which can help us in delaying that object initialisation as per the requirement.
One way to do that is by using lateinit Property where we just declare the object/property and its type but didn’t initialise it.
The drawbacks with lateinit var is
- Its var that means it’s mutable
- We (developer) need to remember to initialise it.
- We can’t have a custom getter to the lateinit property
We have another way also to delay the initialisation of a property ie by using lazy initialisation.
lazy initialisation is a delegation of object creation when the first time that object will be called. The reference will be created but the object will not be created. The object will only be created when the first time that object will be accessed and every next time the same reference will be used.
The basic initialisation of a User class object by using lazy will look like below
data class User(val id : Long,
val username : String)
val user : User by lazy {
//can do other initialisation here
User(id = 1001, username = "ballu")
}
Let’s try to understand what exactly lazy is
lazy
is a function defined in kotlin
package which takes a lambda or higher-order function as a parameter and returns Lazy<T>
object.
The passed lambda or higher-order function should return the object of Template class (T
).
/**
* Creates a new instance of the [Lazy] that uses the specified initialization function [initializer]
* and the default thread-safety mode [LazyThreadSafetyMode.SYNCHRONIZED].
*
* If the initialization of a value throws an exception, it will attempt to reinitialize the value at next access.
*
* Note that the returned instance uses itself to synchronize on. Do not synchronize from external code on
* the returned instance as it may cause accidental deadlock. Also this behavior can be changed in the future.
*/
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
The return type of the above lazy function is a Lazy
interface reference object which is also defined in kotlin
package. This Lazy
reference object server as a delegate for implementation of lazy property.
The first call to property’s get()
executes the lambda passed to the lazy
function and remembers the result. Any subsequent calls to get the property simply return the remembered result.
/**
* Represents a value with lazy initialization.
*
* To create an instance of [Lazy] use the [lazy] function.
*/
public interface Lazy<out T> {
/**
* Gets the lazily initialized value of the current Lazy instance.
* Once the value was initialized it must not change during the rest of lifetime of this Lazy instance.
*/
public val value: T
/**
* Returns `true` if a value for this Lazy instance has been already initialized, and `false` otherwise.
* Once this function has returned `true` it stays `true` for the rest of lifetime of this Lazy instance.
*/
public fun isInitialized(): Boolean
}
Let’s look at the thread-safety of lazy property
By default, the lazy property is synchronised or thread-safe. That means only a single thread will compute its value and all threads will use the same value. But there is an overload to the lazy function where we can pass the LazyThreadSafetyMode
also.
/**
* Creates a new instance of the [Lazy] that uses the specified initialization function [initializer]
* and thread-safety [mode].
*
* If the initialization of a value throws an exception, it will attempt to reinitialize the value at next access.
*
* Note that when the [LazyThreadSafetyMode.SYNCHRONIZED] mode is specified the returned instance uses itself
* to synchronize on. Do not synchronize from external code on the returned instance as it may cause accidental deadlock.
* Also this behavior can be changed in the future.
*/
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
LazyThreadSafetyMode
is an enum class with values as below
/**
* Specifies how a [Lazy] instance synchronizes initialization among multiple threads.
*/
public enum class LazyThreadSafetyMode {
/**
* Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
*/
SYNCHRONIZED,
/**
* Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
* but only the first returned value will be used as the value of [Lazy] instance.
*/
PUBLICATION,
/**
* No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
*
* This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
*/
NONE,
}
All 3 types are explained by themselves.
- SYNCHRONIZED is used to ensure thread safety. Only a single thread will initialise the value and it uses lock to ensure the same.
- PUBLICATION can be called several times but the returned value will be used.
- NONE is a tricky one and not suggested to use until for a specific reason. no locks are used and if accessed from multiple threads its behaviour is undefined.
By default the lazy function uses SYNCHRONIZED but we can change it by passing it as a parameter to the lazy function.
val user : User by lazy(LazyThreadSafetyMode.PUBLICATION) {
//can do other initialisation here
User(id = 1001, username = "ballu")
}
Few things to remember about lazy property
- Every lazy property is a val (immutable) property and cannot be changed once assigned.
- The property initialisation is conditional based and will only be initialised when it will be called the first time.
- By default, the property initialisation is thread-safe.
- We can do other computations also before initialising the actual property in the same lazy function.