1. Introduction

Lazy initialization in Kotlin can be easily mixed up with late initialization. After all, in both cases, the class field isn’t initialized and is given its actual value sometime later. However, is it so simple?

In this article, we’ll look closely at what exactly are both types of initialization and if there are any differences between them.

2. Lazy Initialization

Commonly, lazy initialization in Kotlin means the usage of a delegate function lazy {}. There are several out-of-the-box delegate provider functions, and lazy is one of them. The usual way of declaring a delegate property is with a keyword by:

val specialValue by SomeDelegate(actualValue = "I am Groot")

It is quite difficult to write a thread-safe lazy initialization. Mistakes can be costly, especially if the domain is finance or hardware control. lazy delegate allows us to re-use a tried and tested primitive and side-step all possible issues:

val lazyValue by lazy {
    println("Only now the field is initialized")

The default lazy delegate is kotlin.SynchronizedLazyImpl. It will take a synchronized lock, ensuring there will be only one instantiation during the first read. Every other thread will wait until the one holding the lock finishes. On subsequent reads, there will be no blocking.

It’s worth noting that only immutable val fields can use a standard library lazy primitive. The produced Lazy<T> delegate lacks a setter and therefore can’t be changed.

3. Late Initialization

Late initialization, on the other hand, is a special language keyword:

lateinit var lateValue: ValueType

It can only appear before a var modifier and it can only modify a variable of a non-primitive type. In hindsight, it’s obvious why. If it’s a primitive type and not initialized, it is going to have a default value for that type. If it’s a val, then it can’t change later.

The lateinit keyword is nothing more than a promise to the compiler that this reference will definitely get a value before anybody accesses it. If we break this promise, the code throws UninitializedPropertyAccessException:

class LateinitSample {
    lateinit var lateValue: ValueType

val sample = LateinitSample()
sample.lateValue // This line throws UninitializedPropertyAccessException

The proper use of a lateinit variable would be to initialize it before accessing:

class LateinitSample {
    lateinit var lateValue: ValueType

    fun initBasedOnEnvironment(env: Map<String, String>) {
        lateValue = ValueType(env.toString())

val sample = LateinitSample().apply { 
    initBasedOnEnvironment(mapOf("key" to "value"))
sample.lateValue // Doesn't throw

However, this makes the usage of LateinitSample class cumbersome. There are very few cases where the lateinit keyword is justified and, for the most part, it is best to avoid it.

4. Comparison

So, are lazy and late initialization in any way similar?

Lazy initialization is one of the property Delegate-s, while late initialization requires the use of a language keyword. Lazy initialization applies only to val, and late initialization applies only to var fields. We can have a lazy field of a primitive type, but lateinit applies only to reference types.

Most importantly, when we implement a field as a lazy delegate, we are actually giving it a value of sorts. Instead of an actual value, we put there a function to calculate it if and when we need it. On the other hand, when we declare a field as a lateinit, we just switch off one of the compiler checks that ensure that the program accesses no variable before it receives a value. Instead, we promise to do that check ourselves.

Therefore, it would be fair to say that lazy initialization is something completely different from late initialization.

5. Conclusion

In this tutorial, we compared late and lazy initialization side by side and saw that they are very much not the same thing. During lazy initialization, we provide a way for a field to obtain a value if we access it later. When we use late initialization, we leave a field uninitialized until later, which might get us into trouble.

The implementation of all the examples in the article and code snippets can be found over on GitHub.