In Kotlin, read-only collection interfaces plus copy-on-write updates are a common way to model immutable-style data.
Kotlin has two main collection interface families:
| Mutable | Read-only |
|---|---|
MutableList<T> |
List<T> |
MutableSet<T> |
Set<T> |
MutableMap<K, V> |
Map<K, V> |
Read-only interfaces prevent mutation through that reference, while copy-on-write means you create a new collection when you need a changed version.
Use read-only collection types
Prefer exposing List, Set, and Map instead of mutable versions:
class UserGroup(
val users: List<String>
)
val group = UserGroup(listOf("Alice", "Bob"))
// group.users.add("Charlie") // Does not compile
This prevents callers from mutating the collection directly.
Copy-on-write with lists
To “modify” a list, create a new one:
val users = listOf("Alice", "Bob")
val withCharlie = users + "Charlie"
val withoutAlice = users - "Alice"
println(users) // [Alice, Bob]
println(withCharlie) // [Alice, Bob, Charlie]
println(withoutAlice) // [Bob]
The original list remains unchanged.
Copy-on-write with sets
val roles = setOf("USER", "ADMIN")
val updatedRoles = roles + "AUDITOR"
val reducedRoles = roles - "ADMIN"
println(roles) // [USER, ADMIN]
println(updatedRoles) // [USER, ADMIN, AUDITOR]
println(reducedRoles) // [USER]
Copy-on-write with maps
val settings = mapOf(
"theme" to "dark",
"language" to "en"
)
val updatedSettings = settings + ("theme" to "light")
val removedSetting = settings - "language"
println(settings)
// {theme=dark, language=en}
println(updatedSettings)
// {theme=light, language=en}
println(removedSetting)
// {theme=dark}
Updating objects with immutable collections
This pattern is especially useful with data class and copy():
data class Team(
val name: String,
val members: List<String>
)
val team = Team(
name = "Backend",
members = listOf("Alice", "Bob")
)
val updatedTeam = team.copy(
members = team.members + "Charlie"
)
println(team)
// Team(name=Backend, members=[Alice, Bob])
println(updatedTeam)
// Team(name=Backend, members=[Alice, Bob, Charlie])
Avoid leaking mutable collections
Be careful with this:
val mutableUsers = mutableListOf("Alice", "Bob")
val users: List<String> = mutableUsers
mutableUsers += "Charlie"
println(users) // [Alice, Bob, Charlie]
Even though users is typed as List<String>, the underlying collection is still mutable.
If you need a defensive copy:
val users: List<String> = mutableUsers.toList()
Encapsulate mutation internally
A common pattern is to keep a mutable collection private, but expose a read-only copy:
class UserRegistry {
private val users = mutableListOf<String>()
fun addUser(user: String) {
users += user
}
fun users(): List<String> = users.toList()
}
This prevents external code from modifying your internal list.
Prefer immutable state updates
For state management, prefer this style:
data class AppState(
val selectedIds: Set<Int>,
val usersById: Map<Int, String>
)
val state = AppState(
selectedIds = setOf(1, 2),
usersById = mapOf(
1 to "Alice",
2 to "Bob"
)
)
val newState = state.copy(
selectedIds = state.selectedIds + 3,
usersById = state.usersById + (3 to "Charlie")
)
Use persistent collections for heavy updates
Kotlin’s standard collections are read-only interfaces, but not deeply immutable persistent data structures.
If you frequently create modified copies of large collections, consider kotlinx.collections.immutable:
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
val users: PersistentList<String> = persistentListOf("Alice", "Bob")
val updatedUsers = users.add("Charlie")
println(users) // [Alice, Bob]
println(updatedUsers) // [Alice, Bob, Charlie]
Persistent collections share internal structure, so copy-on-write-style updates are usually more efficient than copying a whole List.
Summary
Use:
val names: List<String> = listOf("Alice", "Bob")
val updated = names + "Charlie"
Instead of:
val names = mutableListOf("Alice", "Bob")
names += "Charlie"
General guidance:
- Expose
List,Set, andMap, not mutable types. - Use
+,-,map,filter, andcopy()to create updated values. - Avoid leaking mutable backing collections.
- Use
toList(),toSet(), ortoMap()for defensive copies. - Use
kotlinx.collections.immutablefor efficient persistent immutable collections.
