1. Overview
In this article, we’re going to learn why we can’t use late-initialized properties and variables for primitive types in Kotlin.
To better understand the rationale behind this limitation, first, we take a step back and see how Kotlin handles nullability for primitive types. Then, we’ll get familiar with the motivation for lateinit properties. In the end, it’ll be much easier to understand why it doesn’t make sense to mix the two.
2. Kotlin and Primitive Types
To better understand how Kotlin compiles the primitive types, let’s consider an example:
class LateInit {
private val nonNullable: Int = 12
private val nullable: Int? = null
// omitted
}
In the above example, we have one nullable and one non-nullable variable with the Int type. Now, let’s compile this file using kotlinc and take a look at the generated bytecode using the javap tool:
>> kotlinc LateInit.kt
>> javap -c -p com.baeldung.lateinit.LateInit
public final class com.baeldung.lateinit.LateInit {
private final int nonNullable;
private final java.lang.Integer nullable;
// omitted
}
Naturally, Kotlin compiles the non-nullable primitive types to their corresponding primitive types in Java – int in this example. Also, it compiles the nullable primitive types to their corresponding boxed types in Java – Integer in this example. The same is true for other primitive types in Kotlin.
3. A lateinit Refresher
In Kotlin, we should always initialize non-null properties in the constructor using a property initializer. Unfortunately, sometimes it may not be possible or convenient to do that. For instance, that can be the case when we’re using a particular dependency injection approach or setup methods in testing frameworks:
@Autowired
private val userService: UserService // won't work
The above example won’t even compile because we’re not initializing a non-nullable property. One approach to fix this is using nullable types:
@Autowired
private val userService: UserService? = null
// on the call site
userService?.createUser(user)
This surely works. However, we should deal with the awkwardness of nullable types (null-safe operators and so on) every time we’re going to use the variable.
To overcome this limitation, Kotlin provides lateinit variables, which can be left uninitialized at first:
@Autowired
private lateinit var userService: UserService
Basically, here we****‘re telling the compiler that we know we didn’t initialize this variable right away, but we promise to do so as soon as possible. If we fail to initialize the property, the runtime will throw an exception once we try to read its value.
This way, we can act as if this variable was non-nullable, which is great! Interestingly, under the hood, Kotlin compiles lateinit variables like normal nullable types. For instance, let’s consider another example:
private lateinit var lateinit: String
The bytecode for the above example will be like:
private java.lang.String lateinit;
As opposed to normal vars, every time we access the lateinit variables, Kotlin checks whether we initialized them:
6: ifnonnull 16
9: ldc #22 // String lateinit
11: invokestatic #28 // Method Intrinsics.throwUninitializedPropertyAccessException:(LString;)V
14: aconst_null
15: athrow
As shown above, if a lateinit variable’s value is null (index 6), then Kotlin will throw an exception at runtime (index 11, 14, and 15).
More specifically, to check if the variable is initialized, Kotlin calls the throwUninitializedPropertyAccessException method and throws an instance of the UninitializedPropertyAccessException exception.
Basically, an extra method call is the price we’re paying to use this syntactic sugar.
4. Primitive Types and lateinit
The bottom line is, Kotlin is using null as a special value to determine if a lateinit property is initialized or not. That is, even though we can re-assign lateinit vars to something else, we can’t set them to null explicitly, as it has a special meaning and is reserved for that.
Now, let’s see what’s wrong with the following declaration:
private lateinit var x: Int
Based on what we learned so far, Kotlin will compile x as an int under the hood. Also, since this is a lateinit variable, Kotlin needs to use null as a special value to represent uninitialized cases. *Because we can’t store null into ints and other primitive types, this declaration is illegal in Kotlin*.
One might propose to use nullable types to solve the above limitation:
private lateinit var x: Int?
Obviously, this does not make sense for two reasons:
- For a nullable type like Int?, all values including nulls are acceptable. So, we can’t use the null as a special holder for uninitialized cases. For this reason, we can’t use lateinit variables with nullable types, either.
- Even if it was possible to implement lateinit primitive variables with nullable types, it doesn’t make sense to do so. As we know, we’re using lateinit variables to avoid the awkwardness of nullable types. So, using nullable types for primitives defeats the whole purpose of using lateinit altogether.
So, to sum up, we can’t use lateinit variables for primitive (such as Int or Boolean) or nullable types in Kotlin.
5. Conclusion
In this article, we learned why Kotlin doesn’t allow using lateinit variables for primitive and any nullable types. More inclined readers can also track the progress of Project Valhalla, which may provide a good solution for this limitation in the near future.
As usual, all the examples are available over on GitHub.