Skip to main content

Kotlin Immutability

Introduction

Immutability is a core concept in functional programming and an important feature in Kotlin. An immutable object is one whose state cannot be modified after it's created. In contrast to mutable objects (which can be changed), immutable objects provide several benefits including thread safety, predictability, and easier reasoning about code.

In this tutorial, we'll explore how Kotlin supports immutability and why it's a fundamental principle in functional programming.

Why Immutability Matters

Before diving into the technical details, let's understand why immutability is valuable:

  1. Thread Safety: Immutable objects can be shared between multiple threads without synchronization.
  2. Predictability: Once created, an immutable object will never change, making code behavior more predictable.
  3. Easy Debugging: Immutable objects can't be modified unexpectedly, making it easier to track the flow of data.
  4. Functional Programming: Immutability is a cornerstone of functional programming, enabling pure functions and referential transparency.

Immutability in Kotlin

1. val vs var Keywords

The most basic form of immutability in Kotlin starts with the val keyword:

kotlin
// Immutable - cannot be reassigned
val immutableName = "John"

// Mutable - can be reassigned
var mutableName = "John"
mutableName = "Jane" // This is allowed

// This would cause a compilation error
// immutableName = "Jane"

It's important to understand that val only makes the reference immutable, not necessarily the object it points to:

kotlin
val list = mutableListOf(1, 2, 3)
// Can't reassign list
// list = mutableListOf(4, 5, 6) // Error

// But can modify its contents
list.add(4) // This is allowed
println(list) // [1, 2, 3, 4]

2. Immutable Collections

Kotlin's standard library provides distinct types for mutable and immutable collections:

kotlin
// Immutable collections
val immutableList = listOf(1, 2, 3)
val immutableSet = setOf("apple", "banana", "orange")
val immutableMap = mapOf("a" to 1, "b" to 2)

// These operations would cause compilation errors
// immutableList.add(4)
// immutableSet.add("grape")
// immutableMap.put("c", 3)

// Mutable collections
val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4) // Works fine

val mutableSet = mutableSetOf("apple", "banana")
mutableSet.add("orange") // Works fine

val mutableMap = mutableMapOf("a" to 1, "b" to 2)
mutableMap["c"] = 3 // Works fine

Converting between mutable and immutable collections:

kotlin
// From mutable to immutable
val mutableNumbers = mutableListOf(1, 2, 3)
val immutableNumbers = mutableNumbers.toList()

// From immutable to mutable
val readOnlySet = setOf("a", "b", "c")
val editableSet = readOnlySet.toMutableSet()

3. Data Classes and Immutability

Data classes in Kotlin are excellent for creating immutable data structures:

kotlin
data class Person(val name: String, val age: Int)

// Create an instance
val person = Person("Alice", 30)

// Can't modify properties
// person.name = "Bob" // Error
// person.age = 31 // Error

// Instead, create a new instance with copy()
val updatedPerson = person.copy(age = 31)
println(person) // Person(name=Alice, age=30)
println(updatedPerson) // Person(name=Alice, age=31)

The copy() method is automatically generated for data classes, providing a convenient way to create modified versions of immutable objects.

Achieving Deep Immutability

To achieve true immutability, you need to ensure that all properties of an object are also immutable:

kotlin
// Not fully immutable
data class Team(val name: String, val members: MutableList<String>)

val team = Team("Engineering", mutableListOf("Alice", "Bob"))
team.members.add("Charlie") // This modifies the internal state!

// Better: fully immutable
data class ImmutableTeam(val name: String, val members: List<String>)

val immutableTeam = ImmutableTeam("Engineering", listOf("Alice", "Bob"))
// immutableTeam.members.add("Charlie") // Compilation error

Practical Example: Working with Immutable State

Let's look at a typical case where immutability is beneficial - managing state in an application:

kotlin
// State management with immutability
data class AppState(
val user: User,
val preferences: Preferences,
val currentPage: String
)

data class User(val id: String, val name: String)
data class Preferences(val darkMode: Boolean, val fontSize: Int)

// Initial state
val initialState = AppState(
user = User("1", "Guest"),
preferences = Preferences(darkMode = false, fontSize = 12),
currentPage = "home"
)

// Function to update state (returns new state, doesn't modify original)
fun login(state: AppState, userId: String, userName: String): AppState {
return state.copy(user = User(userId, userName))
}

fun toggleDarkMode(state: AppState): AppState {
val newPreferences = state.preferences.copy(
darkMode = !state.preferences.darkMode
)
return state.copy(preferences = newPreferences)
}

// Usage
var currentState = initialState
println("Initial state: $currentState")

// Login user
currentState = login(currentState, "123", "John Doe")
println("After login: $currentState")

// Toggle dark mode
currentState = toggleDarkMode(currentState)
println("After toggle dark mode: $currentState")

Output:

Initial state: AppState(user=User(id=1, name=Guest), preferences=Preferences(darkMode=false, fontSize=12), currentPage=home)
After login: AppState(user=User(id=123, name=John Doe), preferences=Preferences(darkMode=false, fontSize=12), currentPage=home)
After toggle dark mode: AppState(user=User(id=123, name=John Doe), preferences=Preferences(darkMode=true, fontSize=12), currentPage=home)

This pattern is common in modern frontend frameworks, where each state change produces a new state rather than modifying existing state.

Performance Considerations

While immutability has many benefits, it may introduce performance overhead due to object creation. Kotlin mitigates this with:

  1. Structural Sharing: Many immutable collections use structural sharing to minimize memory usage.
  2. Optimized Copy: Operations like copy() on data classes are optimized.
  3. Persistent Collections: Libraries like Arrow provide efficient persistent data structures.

For most applications, the benefits of immutability outweigh the performance costs. However, in performance-critical sections, you may need to consider mutable alternatives.

Best Practices for Immutability in Kotlin

  1. Use val by default - Only use var when reassignment is necessary
  2. Use immutable collections by default - Only use mutable collections when necessary
  3. Make classes immutable - Use read-only properties and data classes
  4. Return copies rather than exposing internal state - Prevent indirect mutation
  5. Use copy() for creating modified versions - Avoid builders for immutable objects

Summary

Immutability is a powerful concept in Kotlin that aligns well with functional programming principles. By making objects unchangeable after creation, we gain benefits like thread safety, predictability, and simpler reasoning about our code.

Kotlin supports immutability through:

  • The val keyword for immutable references
  • Immutable collection types
  • Data classes with read-only properties
  • The copy() method for creating modified versions of objects

By embracing immutability, you'll write more robust, maintainable, and concurrent-safe code.

Additional Resources

Exercises

  1. Create an immutable Product data class with properties for name, price, and category.
  2. Implement a shopping cart using immutable collections and functions that add/remove items without modifying the original cart.
  3. Convert a mutable class to an immutable one and provide methods to create modified versions of it.
  4. Implement a simple state management system using immutable objects to track application state changes.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)