Question

Kotlin List vs MutableList: Why add, remove, and put Are Missing

kotlincollections

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 view
  • MutableList<T>: can be changed
  • Map<K, V>: read-only view
  • MutableMap<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:

  • List means: 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.

  • List is a visitor pass: you can enter, look around, and count things, but you cannot move furniture.
  • MutableList is 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()."

Take Quiz

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()
  • items is 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.

Take Quiz

Real Codebase Usage

In real Kotlin projects, developers usually follow this rule:

  • use MutableList and MutableMap only where mutation is required
  • expose List and Map whenever 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:

  • List is read-only
  • MutableList is writable
  • Map is read-only
  • MutableMap is writable

Choose the type based on intended use, not just the underlying implementation.

Comparisons

ConceptKotlin TypeCan Read?Can Modify?Typical Use
Read-only listList<T>YesNoExpose or consume data safely
Mutable listMutableList<T>YesYesBuild or update collections
Read-only mapMap<K, V>YesNoLookup data without mutation
Mutable mapMutableMap<K, V>YesYesCaches, counters, configuration

List vs MutableList

FeatureListMutableList
get(index)YesYes
iterationYesYes
sizeYesYes
add()NoYes
remove()NoYes
clear()NoYes

Java List vs Kotlin List

LanguageTypeIncludes mutation methods?
JavaListYes
KotlinListNo
KotlinMutableListYes

ArrayList vs MutableList

TypeWhat it represents
MutableListAn interface describing mutable list behavior
ArrayListOne concrete implementation of a mutable list

In Kotlin, prefer coding to MutableList unless you specifically need ArrayList behavior.

Take Quiz

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 List when you only need to read.
  • Use MutableList when you need add, remove, or clear.
  • Use Map when you only need lookup.
  • Use MutableMap when you need put, assignment, or removal.
  • Avoid casting just to mutate.
  • Avoid !! unless you truly cannot avoid it.
  • Prefer mutableListOf() and mutableMapOf() 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more