When declaring properties, it’s crucial to determine whether a property should be mutable, as this decision can directly impact your software’s behavior. This is a fundamental consideration, as it can lead to potential issues in your code if not handled carefully.
When using a state management library like Jetpack Compose Runtime, the immutability of properties becomes crucial. It directly impacts whether or not a composable function can be determined as skippable, affecting your application’s performance.
This article explores the intriguing question of whether a property defined with the val
keyword in Kotlin is truly immutable or simply read-only — an interesting topic recently raised in Dove Letter. Dove Letter is a subscription repository where you can learn, discuss, and share new insights about Android and Kotlin. If you’re interested in joining, be sure to check out “Learn Kotlin and Android With Dove Letter.”
Importance of Immutability
There are several critical advantages of declaring properties as immutable whenever possible in the programming world:
- Predictability and Safety: Immutable properties can’t be changed after initialization, making it easier to understand and predict their behavior, reducing the risk of unintended side effects.
- Avoiding Side Effects: Mutable properties can lead to unpredictable behavior. Immutability keeps the state stable, reducing the chance of bugs.
- Functional Programming: Many modern paradigms, like functional programming, emphasize immutability, leading to more modular, reusable, and maintainable code that’s easier to scale.
- Simplified State Management: Immutability simplifies state management, especially in like Jetpack Compose, where stable and immutable objects improve performance by reducing unnecessary recompositions.
- Thread Safety: Immutable properties are inherently thread-safe, preventing concurrency issues like race conditions without needing complex synchronization.
You’ve understood the importance of immutability and how it impacts various aspects of software quality. Now, let’s dive deeper into how immutability applies to properties in Kotlin.
Immutable vs. Read-Only
In Kotlin, you can declare properties as mutable using the var
keyword or as non-reassignable using the val
keyword, preventing changes to their value. But does declaring a property with val
truly makes it immutable?
Curious about this topic, I created a poll to see how the Android community typically approaches the immutability of Kotlin properties. Here are the results:
As you can see from the poll, out of 247 participants, 59% consider the val
property to be read-only, while 41% believe it is truly immutable.
If you read Kotlin’s official documentation on properties, you’ll find that there are two ways to declare properties: var
and val
, as described below:
Properties in Kotlin classes can be declared either as mutable, using the
var keyword, or as read-only, using the
val keyword.
Surprisingly, the official documentation doesn’t mention “immutable” or “immutability” anywhere. It exclusively refers to val
properties as “read-only.” The reasoning is simple: two clear distinctions explain why a val
property is considered read-only but not truly immutable in most cases.
- Objects can still be modified
Even when a property is declared with the val
keyword, the object it refers to can still be changed. For example, consider the following property:
val myList = mutableListOf("Item1", "Item2") | |
myList.add("Item3") // Modifying the list, but the reference remains the same |
Although the reference myList
cannot be reassigned, the list items can still be modified. Let’s see another example.
class Sample { | |
private val text: String = "title" | |
} |
If you decompile the Sample
class and examine the Kotlin bytecode, you’ll see how it is ultimately transformed in the JVM environment.
private final Ljava/lang/String; text | |
@Lorg/jetbrains/annotations/NotNull;() // invisible |
Job Offers
val
properties in Kotlin are compiled to final
in the bytecode, which, according to the Java Language Specification, means the following:
Once a
final
variable has been assigned, it always contains the same value. If afinal
variable holds a reference to an object, then the state of the object may be changed by operations on the object, but the variable will always refer to the same object. This applies also to arrays, because arrays are objects; if afinal
variable holds a reference to an array, then the components of the array may be changed by operations on the array, but the variable will always refer to the same array.
Eventually, the val
keyword only ensures the reference is constant but does not guarantee the object’s immutability.
2. Classes and interfaces can still override properties
The other reason is that a class or interface can still override the property, meaning there’s a possibility for the value of a val
property to change. This makes val
not strictly immutable in all cases. For example, consider the State
interface in Jetpack Compose:
interface State<out T> { | |
val value: T | |
} | |
interface MutableState<T> : State<T> { | |
override var value: T | |
operator fun component1(): T | |
operator fun component2(): (T) -> Unit | |
} | |
val state: State<String> = remember { mutableStateOf("text") } | |
(state as MutableState<String>).value = "changed" |
Even though value
is declared as val
, subclasses can override this property, potentially changing its behavior or value. This flexibility shows that val
ensures read-only access to the reference but doesn’t fully guarantee immutability, especially when inheritance or overriding is involved.
This clarifies the distinction: it’s clearer to describe the val
property as reference immutability (the reference cannot change) & object mutability (the object itself can be modified) rather than simply calling it “read-only.”
Immutable Object
So, when you define an object, is it truly immutable? For a class to be genuinely immutable, it must consist entirely of read-only properties, and those properties must be either primitive types or objects that do not allow any changes to their internal states, but it should also prevent overriding by using declaring as a data class. Consider the following data class example:
data class Sample( | |
val name: String, | |
val url: String, | |
val age: Int | |
) |
Data classes in Kotlin don’t allow inheritance, so if they consist entirely of immutable properties, you can consider them truly immutable objects. When decompiled into bytecode, you’ll see that they are defined as final
classes with final
field properties, ensuring they cannot be modified or extended.
public final class com/skydoves/server/driven/core/model/Sample { | |
// access flags 0x12 | |
private final Ljava/lang/String; name | |
@Lorg/jetbrains/annotations/NotNull;() // invisible | |
// access flags 0x12 | |
private final Ljava/lang/String; url | |
@Lorg/jetbrains/annotations/NotNull;() // invisible | |
// access flags 0x12 | |
private final I age | |
} |
Conclusion
In this article, you’ve explored the importance of immutability in software and examined whether properties defined with val
in Kotlin are truly immutable. While immutability can be interpreted in various ways depending on context and perspective, understanding the fundamental concepts of immutability in your primary programming language is crucial for reducing bugs and improving maintainability.
If you have any questions or feedback on this article, you can find the author on Twitter @github_skydoves or GitHub. If you’d like to stay updated with the latest information through articles and references, tips with code samples that demonstrate best practices, and news about the overall Android/Kotlin ecosystem, check out ‘Learn Kotlin and Android With Dove Letter’.
As always, happy coding!
— Jaewoong
This article is previously published on proandroiddev.com