Question
In Java, you can declare a variable using the List interface and still call methods such as add() and remove() when the actual object is an ArrayList.
public class TempClass {
List<Integer> myList = null;
void doSomething() {
myList = new ArrayList<>();
myList.add(10);
myList.remove(10);
}
}
When translating this directly to Kotlin, the equivalent code does not work:
class TempClass {
var myList: List<Int>? = null
fun doSomething() {
myList = ArrayList<Int>()
myList!!.add(10)
myList!!.remove(10)
}
}
Kotlin reports that add() and remove() do not exist on List.
A workaround is to cast the value to ArrayList, but that feels incorrect and removes the benefit of programming to the abstract type:
class TempClass {
var myList: List<Int>? = null
fun doSomething() {
myList = ArrayList<Int>()
(myList!! as ArrayList<Int>).add(10)
(myList!! as ArrayList<Int>).remove(10)
}
}
Why does Kotlin behave differently from Java here, and how can you use a list or map in Kotlin without needing unsafe casts when you want to modify it?
Short Answer
By the end of this page, you will understand why Kotlin separates read-only collection types like List and Map from mutable collection types like MutableList and MutableMap. You will learn when to use each one, how to write modifiable collections correctly, and why casting is usually the wrong fix.
Concept
Kotlin intentionally separates read-only and mutable collection interfaces.
In Java, List includes both reading and writing operations:
- read:
get(), iteration,size - write:
add(),remove(),clear()
In Kotlin, these responsibilities are split:
List<T>: read-only viewMutableList<T>: can be changedMap<K, V>: read-only viewMutableMap<K, V>: can be changed
That is why this does not compile:
var myList: List<Int>? = null
myList!!.add(10) // Error
The variable type is List<Int>, and List does not promise mutation methods.
Why Kotlin does this
This design makes code safer and clearer.
When a function accepts List<T>, it communicates:
- "I only need to read from this collection."
- "I will not modify it."
When a function accepts MutableList<T>, it communicates:
- "This function may change the collection."
That distinction helps developers understand intent immediately and prevents accidental modification.
The correct fix
If you need to modify the collection, declare it as mutable:
class TempClass {
var myList: MutableList<Int>? = null
fun doSomething() {
myList = ArrayList()
myList!!.add(10)
myList!!.remove(10)
}
}
Even better, avoid nullable collections unless null is truly meaningful:
class TempClass {
private val myList = mutableListOf<Int>()
fun doSomething() {
myList.add(10)
myList.remove(10)
}
}
Important detail: read-only is not always fully immutable
List in Kotlin is read-only, not necessarily deeply immutable.
For example:
val items: List<Int> = mutableListOf(1, 2, 3)
Through items, you cannot call add(). But the underlying object may still be mutable if another reference points to it as a MutableList.
So the key idea is:
Listmeans: you cannot mutate through this reference- not necessarily: the object can never change anywhere
This is one of the most important collection concepts in Kotlin.
Mental Model
Think of Kotlin collections like access passes to a room.
Listis a visitor pass: you can enter, look around, and count things, but you cannot move furniture.MutableListis a staff pass: you can enter and rearrange the room.
The actual room might be the same ArrayList, but your pass determines what actions are allowed.
So if your variable is typed as List, Kotlin says: "You only asked for a visitor pass, so you cannot use staff-only actions like add() or remove()."
Syntax and Examples
Core syntax
Read-only list
val numbers: List<Int> = listOf(1, 2, 3)
println(numbers[0])
println(numbers.size)
You can read values, but not change the list.
Mutable list
val numbers: MutableList<Int> = mutableListOf(1, 2, 3)
numbers.add(4)
numbers.remove(2)
println(numbers)
You can both read and modify it.
Read-only map
val ages: Map<String, Int> = mapOf("Ana" to 25, "Ben" to 30)
println(ages["Ana"])
Mutable map
val ages: MutableMap<String, Int> = mutableMapOf("Ana" to 25)
ages["Ben"] = 30
ages.put("Cara", 28)
println(ages)
Your example written correctly
class TempClass {
private val myList: MutableList<Int> = ArrayList()
fun doSomething() {
myList.add(10)
myList.remove(10)
}
}
Prefer factory functions
In Kotlin, developers often use collection factory functions instead of Java-style constructors:
val names = mutableListOf("Ava", "Noah")
val scores = mutableMapOf("Ava" to 100)
These are concise and idiomatic.
If you only need reading
fun printItems(items: List<String>) {
for (item in items) {
println(item)
}
}
Using List<String> here is better than MutableList<String> because the function does not need to modify anything.
Step by Step Execution
Consider this example:
fun main() {
val items: MutableList<String> = mutableListOf()
items.add("apple")
items.add("banana")
items.remove("apple")
println(items)
}
Step by step
1. Create an empty mutable list
val items: MutableList<String> = mutableListOf()
itemsis a variable that refers to a mutable list.- The list starts empty:
[].
2. Add apple
items.add("apple")
- The list becomes:
["apple"].
3. Add banana
items.add("banana")
- The list becomes:
["apple", "banana"].
4. Remove apple
items.remove("apple")
- Kotlin searches for the matching element.
- The list becomes:
["banana"].
5. Print the final list
println(items)
Output:
[banana]
What if the type were List<String> instead?
val items: List<String> = mutableListOf()
items.add("apple") // Error
Even though the underlying object is mutable, the reference type is read-only, so mutation methods are hidden.
Real World Use Cases
1. Passing safe read-only data to functions
A service may return a List<User> so callers can read the users without modifying the internal collection.
fun getUsers(): List<User> = users
2. Building collections during processing
When collecting results step by step, use MutableList.
val errors = mutableListOf<String>()
if (name.isBlank()) errors.add("Name is required")
if (email.isBlank()) errors.add("Email is required")
3. Updating configuration or caches
A cache often needs a MutableMap because entries are added and replaced over time.
val cache = mutableMapOf<String, String>()
cache["theme"] = "dark"
4. API response transformation
You might read from a List returned by an API and build a new mutable result list.
val result = mutableListOf<String>()
for (user in apiUsers) {
result.add(user.name)
}
5. Exposing internal data safely
A class may keep a MutableList internally but expose it as List publicly.
class Store {
private val _items = mutableListOf<String>()
val items: List<String> get() = _items
fun addItem(item: String) {
_items.add(item)
}
}
This is a common Kotlin pattern.
Real Codebase Usage
In real Kotlin projects, developers usually follow this rule:
- use
MutableListandMutableMaponly where mutation is required - expose
ListandMapwhenever possible
Common patterns
1. Private mutable, public read-only
class Cart {
private val _products = mutableListOf<String>()
val products: List<String> get() = _products
fun addProduct(product: String) {
_products.add(product)
}
}
This prevents outside code from changing the collection directly.
2. Build mutable, return read-only
fun loadNames(): List<String> {
val result = mutableListOf<String>()
result.add("Ava")
result.add("Leo")
return result
}
The function uses mutation internally but returns a read-only type.
3. Validation and accumulation
fun validateUser(name: String, email: String): List<String> {
val errors = mutableListOf<String>()
if (name.isBlank()) return listOf("Name is required")
if (email.isBlank()) errors.add("Email is required")
return errors
}
This combines mutation with guard clauses and early returns.
4. Transform instead of mutate when possible
Kotlin codebases often prefer map, filter, and sortedBy for producing new collections instead of changing old ones.
val activeNames = users
.filter { it.isActive }
.map { it.name }
This style is often easier to read and reason about.
5. Mutable maps for counters and indexes
val counts = mutableMapOf<String, Int>()
for (word in words) {
counts[word] = (counts[word] ?: 0) + 1
}
This is common in parsing, analytics, and data processing.
Common Mistakes
1. Declaring List when you need MutableList
Broken code:
val items: List<Int> = mutableListOf()
items.add(1) // Error
Fix:
val items: MutableList<Int> = mutableListOf()
items.add(1)
2. Casting just to force mutation
Broken code:
val items: List<Int> = mutableListOf(1, 2, 3)
(items as ArrayList<Int>).add(4)
Why this is bad:
- unsafe
- tightly couples your code to a specific implementation
- may fail at runtime if the object is not actually an
ArrayList
Better:
val items: MutableList<Int> = mutableListOf(1, 2, 3)
items.add(4)
3. Using !! unnecessarily
Broken code:
var items: MutableList<Int>? = null
items!!.add(1)
This can throw a NullPointerException.
Better:
val items = mutableListOf<Int>()
items.add(1)
Or, if null is required:
items?.add(1)
4. Assuming List means completely immutable
val mutable = mutableListOf(1, 2, 3)
val readOnly: List<Int> = mutable
mutable.add(4)
println(readOnly) // [1, 2, 3, 4]
readOnly cannot modify the list, but it can still observe changes made elsewhere.
5. Mixing Java collection expectations with Kotlin collection types
Java habits can cause confusion. In Kotlin:
Listis read-onlyMutableListis writableMapis read-onlyMutableMapis writable
Choose the type based on intended use, not just the underlying implementation.
Comparisons
| Concept | Kotlin Type | Can Read? | Can Modify? | Typical Use |
|---|---|---|---|---|
| Read-only list | List<T> | Yes | No | Expose or consume data safely |
| Mutable list | MutableList<T> | Yes | Yes | Build or update collections |
| Read-only map | Map<K, V> | Yes | No | Lookup data without mutation |
| Mutable map | MutableMap<K, V> | Yes | Yes | Caches, counters, configuration |
List vs MutableList
| Feature | List | MutableList |
|---|---|---|
get(index) | Yes | Yes |
| iteration | Yes | Yes |
size | Yes | Yes |
add() | No | Yes |
remove() | No | Yes |
clear() | No | Yes |
Java List vs Kotlin List
| Language | Type | Includes mutation methods? |
|---|---|---|
| Java | List | Yes |
| Kotlin | List | No |
| Kotlin | MutableList | Yes |
ArrayList vs MutableList
| Type | What it represents |
|---|---|
MutableList | An interface describing mutable list behavior |
ArrayList | One concrete implementation of a mutable list |
In Kotlin, prefer coding to MutableList unless you specifically need ArrayList behavior.
Cheat Sheet
// Read-only list
val a: List<Int> = listOf(1, 2, 3)
// Mutable list
val b: MutableList<Int> = mutableListOf(1, 2, 3)
b.add(4)
b.remove(2)
// Read-only map
val c: Map<String, Int> = mapOf("x" to 1)
// Mutable map
val d: MutableMap<String, Int> = mutableMapOf("x" to 1)
d["y"] = 2
d.put("z", 3)
Rules to remember
- Use
Listwhen you only need to read. - Use
MutableListwhen you needadd,remove, orclear. - Use
Mapwhen you only need lookup. - Use
MutableMapwhen you needput, assignment, or removal. - Avoid casting just to mutate.
- Avoid
!!unless you truly cannot avoid it. - Prefer
mutableListOf()andmutableMapOf()for Kotlin-style code.
Key idea
The declared type controls what methods are available, not just the actual object type.
val items: List<Int> = mutableListOf(1, 2, 3)
// items.add(4) // not allowed
Edge case
List is read-only, not guaranteed deeply immutable.
Another mutable reference can still change the same underlying collection.
FAQ
Why does Kotlin List not have add()?
Because Kotlin separates read-only and mutable collection interfaces. List is for reading, while MutableList is for modification.
How do I add items to a list in Kotlin?
Declare the variable as MutableList<T> and create it with mutableListOf() or ArrayList().
val items = mutableListOf<Int>()
items.add(1)
Why does Kotlin Map not have put()?
For the same reason. Map is read-only, and MutableMap is the type that supports put(), assignment, and removal.
Can I cast List to ArrayList in Kotlin?
You can, but it is usually a bad idea. It is unsafe and depends on the actual implementation. Prefer declaring MutableList from the start.
Is Kotlin List immutable?
Not always. It is read-only through that reference, but the underlying object may still be mutable elsewhere.
Should I use ArrayList or MutableList?
Usually MutableList. Use the interface unless you specifically need ArrayList itself.
How should I expose collections from a class?
A common pattern is to keep a private MutableList internally and expose a public List.
Should a collection be nullable?
Usually no. Prefer an empty list instead of null unless null has a specific meaning in your design.
Mini Project
Description
Create a small inventory tracker for a shop. This project demonstrates when to use MutableList internally to update data and when to expose a read-only List to the rest of the program. It mirrors how Kotlin collections are commonly used in real applications.
Goal
Build a class that stores product names, allows products to be added and removed, and exposes the current inventory safely as a read-only list.
Requirements
- Create a class named
Inventory. - Store products internally in a mutable collection.
- Expose the product list as a read-only
List<String>. - Add a function to add a product.
- Add a function to remove a product.
- Print the inventory before and after updates.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.