Lazy vs Lateinit vs Nullable

In this tutorial we will cover the different options Kotlin provides, to implement the lazy initialization pattern. We will point out how to use them and which to choose.

Kotlin provides three build-in ways to implement this pattern.

What is the lazy pattern?

The lazy initialization pattern, also called deferred initialization, is used to postpone the creation of an object to a later time point. In most cases, member variables are initialized when their parent object is created. In some cases, however, it is advantageous to postpone the creation to a later point. This can happen for example if it takes a lot of time to create the object and it makes sense to postpone it to the actual time point when it is used. Another reason can be that at the point of the parent object creation we just don’t have access to the concrete objects. An example could be the Activity class of an Android application.

By Lazy Delegate

Kotlin provides a prebuild property delegate that can wrap any object or member variable. If you´re not sure how to use delegates in Kotlin, you can check out our Delegation tutorial.

The drawback of using this method is, that a member wrapped by this delegate can not be reassigned. The problem is that lazy does not implement the setValue function. Therefore it can only be used for read-only val during the first assignment. Even if you implement the missing function as an extension function, the cached value is of type val.

The following code shows a simple use case. The lazyVal is assigned on first access.

class LazyExample {

    val lazyVal: Int by lazy {
        println("LazyVal init")
        1
    }

    init {
        println("LazyExample init")
    }
}


fun main() {
    var lazy = LazyExample()
    println(lazy.lazyVal)
    println(lazy.lazyVal)
}

The output of this code is the following. As you can see is function body of the delegate only evaluated once. Afterwards it uses the cached value.

LazyExample init
LazyVal init
1
1

Thread safety

To create a new Lazy object you have to use a specific initialization function initializer. By default, this function is thread-safe. Note that the returned instance uses itself to synchronize. It may cause deadlocks if you try to synchronize from external code to synchronize the wrapped member variable.

The following code is taken from the native Kotlin API (source code). It shows the different options to create thread (un)safe code. You can also see that it may throw exceptions. We recommend using the standard mode (thread-safe) to avoid any conflicts.

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> if (isExperimentalMM()) SynchronizedLazyImpl(initializer) else throw UnsupportedOperationException()
            LazyThreadSafetyMode.PUBLICATION -> if (isExperimentalMM()) SafePublicationLazyImpl(initializer) else FreezeAwareLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }

Pro and Contra

Pro

  • thread save
  • no need to check if initialized

Contra

  • only read only (with standard implementation)

Lateinit keyword

The lateinit keyword can not be used on primitive types. This means in order to use it you have to have a proper class. In reality, this is not a problem because the primitive types are unlikely used for lazy initialization. Furthermore, it can only be used for mutable types. In other words for variables (var).

Android example

This approach is often used in Androids Activity classes (applies as well to Fragments). Usually, in such classes, you provide binding to UI elements that are defined in XML files. But you only have access to these objects after a specific function of the Activity is called (onCreate). Therefore you are forced to delay the assignment of e.g. buttons to the point where you have access to the UI elements.

class MainActivity : AppCompatActivity() {
    
    private lateinit var button: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button = findViewById(R.id.myButton)
}

Check if initialized

A big problem with this keyword is, that you have a potential exception in your code. You can access a lateinit variable before it is actually assigned (the compiler does not complain). But this will throw an exception.

class LateInitExample {
    lateinit var lateInit : NotPrimitive

    fun isInit() : Boolean {
        return this::lateInit.isInitialized
    }
}


fun main() {
    var obj = LateInitExample()
    if(obj.isInit())
        println(obj.lateInit)
}

Thread safety

This approach is not thread-safe. You have to make sure to have the initialization properly handled in case of multiple threads.

Pro and contra

Pro

  • easy to use (no overhead)
  • works on var and val

Contra

  • not thread save
  • need to check (be sure) it is initialized
  • does not work on primitives

Nullable object

By default, every (member) variable must be nonnull in Kotlin. This constraint however can be weakened to make it nullable. In a sense, if we remove this constraint, Kotlin behaves much more like Java code.

The following example shows how to use a nullable object. By using the “?” modifier on the object it is declared as nullable. It can have the state “null”.

class NullableObject {
    var nullable : Int? = null
}

fun main() {
    var obj = NullableObject()
    obj.nullable = 2
    if( obj.nullable != null)
        println(obj.nullable!!)
}

Thread safety

By default, this approach is not thread-safe. You have to make sure it is properly initialized when using it in multiple threaded environments.

Check if not null

One of the big problems with that approach is that you have a potential risk of a NullPointerException. Thus you need to check if the object is null or not null (see above). Kotlin helps you in a way that you need to explicitly indicate if the variable is safe to use. This check is done at compile-time and is only a helper to make you think about it (using “!!“).

Pro and contra

Pro

  • works on var and val
  • works with every type
  • extra state for a variable

Contra

  • not thread save
  • potential exceptions
  • change in writing code (“?”, and “!!”)

Summary

We saw that all approaches had their strong and weak points.

We do not recommend the usage of nullable types. Kotlin puts are a great deal to avoid exceptions. Removing this constraint is counterproductive.

We recommend the usage of lateinit keyword in the use case that you don’t have access to the object when the parent is initialized (as it can be seen in the Android example).

For all other use cases, we recommend the usage of the Lazy Delegate. The strong point is the build in thread safety.