List and MutableList — order, indices, basic operations

Open

1. List and MutableList: what they are and how they differ

If an array in Kotlin is a “fixed-size box” (like a warehouse container: however many slots you got, that’s how many you have), then List is a “list” that can grow and shrink as the program runs. But the main thing that makes List a list is order: elements are stored in a specific sequence, and each element has a position (an index). Kotlin explicitly describes List as a collection with ordered storage and indexed access.

At the same time, it’s important to remember a subtlety: List is a read-only interface. That means through a reference of type List you can read, but cannot modify the list. If you need to add, delete, or replace elements, you use MutableList (it extends list capabilities with write operations).

A small reference table:

Type Can read by index Can add/remove Typical way to create
List<T>
yes no
listOf(...)
MutableList<T>
yes yes
mutableListOf(...)

2. Indices and element access

Order and indices: why the first element is 0

When you work with a list, you almost always think “first”, “second”, “last”. The computer says “0th”, “1st”, “2nd”… and so on. Indices in Kotlin start at zero and go up to lastIndex, where lastIndex = size - 1. This is not a “Kotlin quirk”, but a common convention across most languages.

It’s useful to keep a short diagram in mind:

size = 4
indices: 0  1  2  3
           ^        ^
        first     lastIndex

And this is where the classic beginner mistake is born: trying to access list[size] as the “last element”. Unfortunately, size is the number of elements, not the last index. The last index is size - 1 or lastIndex.

Mini example:

fun main() {
    val xs = listOf(10, 20, 30, 40)

    println(xs.size)       // 4
    println(xs.lastIndex)  // 3
}

Reading elements: list[i] and get(i)

Once you’ve accepted the philosophy “indices start at zero”, the next step is learning to retrieve an element. In Kotlin there are two forms that do the same thing: list[i] and list.get(i). The first is easier to read; the second is sometimes useful when you want to emphasize that this is a method call.

Example:

fun main() {
    val words = listOf("one", "two", "three", "four")

    println(words[0])        // one
    println(words.get(2))    // three
    println(words[3])        // four
}

If the index is invalid (for example, -1 or size), the program will crash with an out-of-bounds error. That’s normal: the list protects itself from “fetch me an element from a parallel universe”.

3. Basic operations of MutableList

Replacing an element by index

When you move from List to MutableList, the magic begins: we can modify elements. The simplest operation is to replace the value at an existing position. The syntax looks as if you were working with an array: list[i] = newValue.

Internally, this is equivalent to set(i, newValue), but the [] notation reads more naturally.

Example:

fun main() {
    val items = mutableListOf("a", "b", "c")

    items[1] = "B"          // replaced the element with index 1
    println(items)          // [a, B, c]
}

The important meaning here: we replaced the element at position 1. We didn’t “insert” anything, the size didn’t change, and other elements didn’t shift.

Adding elements: add(value)

Now let’s talk about growing a list. In real tasks you almost always start with an empty list and gradually fill it: the user enters tasks, products go into a cart, expense entries, participant names — anything.

MutableList.add(value) adds an element to the end of the list. This is one of the most common operations, and it’s convenient to think of it as “append to the end”.

Example:

fun main() {
    val numbers = mutableListOf(1, 2, 3)

    numbers.add(4)
    numbers.add(5)

    println(numbers)        // [1, 2, 3, 4, 5]
    println(numbers.size)   // 5
}

If you feel a little joy when size grows, congratulations: you’ve just experienced your “first container growth”. An array can’t do that.

Deleting by index: removeAt(index) and the shifting effect

Deletion is where indices start behaving like indices. After you delete one element, all elements to the right “slide left”, and their indices change. This is logical: if one person leaves a queue, everyone behind moves closer to the front.

removeAt(index) deletes the element at position index and returns the removed element. This is convenient: you can immediately print what was removed, or use the value later.

Example:

fun main() {
    val items = mutableListOf("x", "y", "z")

    val removed = items.removeAt(0)
    println(removed)        // x
    println(items)          // [y, z]
}

A visual shifting diagram:

was:    [ x, y, z ]
indices  0  1  2

removeAt(0)

now:   [ y, z ]
indices  0  1

If you store “important numbers” of elements (for example, the user selected “delete item #2”), then after deletion the numbers of the remaining items change. That’s not a bug, that’s math.

Deleting by value: remove(value)

MutableList has one more deletion method: remove(value). Unlike removeAt, it doesn’t work with a position, but with the value itself: “remove the element "bread"”.

Two important ideas start here.

First: remove(value) returns a Boolean. true — if deletion happened; false — if that value wasn’t present (i.e., the list didn’t change).

Second: if there are multiple equal values, remove(value) removes the first matching occurrence, not “all at once”.

Example:

fun main() {
    val products = mutableListOf("milk", "bread", "tea", "bread")

    val ok1 = products.remove("bread")
    println(ok1)            // true
    println(products)       // [milk, tea, bread]

    val ok2 = products.remove("coffee")
    println(ok2)            // false
    println(products)       // [milk, tea, bread]
}

Notice: "bread" was there twice, and after deletion one instance remains. That’s normal — we removed only the first occurrence.

Checking membership: contains(value) and the in operator

When you work with lists, you often need the question “is this already there?”. For example, “is this product already in the cart?”, “is this command supported?”, “is this task already added?”.

You can write list.contains(value), or you can write the more readable version — value in list. They mean the same thing; the second is closer to human language.

Mini example:

fun main() {
    val allowed = listOf("add", "list", "remove")

    println(allowed.contains("list"))  // true
    println("add" in allowed)          // true
    println("exit" in allowed)         // false
}

The list can contain duplicates (unlike Set), and contains / in only answer “is there at least one such element or not”. Kotlin emphasizes that List allows duplicates and stores elements in order.

4. Practice: “Shopping list v0”

Now we’ll build a small fragment of an app that will grow along with the course. For now, without complex commands and without architecture: we’ll simply show how MutableList helps store data, and how user input helps add and remove items.

We’ll keep the model as simple as possible: a list of strings MutableList<String>, where each string is a product.

Printing the list “for humans”

Let’s start with a print function. We already know loops, so we’ll just iterate through the products and show them with numbers. Notice: a human-friendly number starts at 1, but the index in the list starts at 0. So “human number = index + 1”.

fun printShoppingList(items: List<String>) {
    println("Shopping list:")
    for (i in 0..items.lastIndex) {
        println("${i + 1}. ${items[i]}")
    }
}

If the list is empty, 0..items.lastIndex will look weird (for example, 0..-1). To avoid surprises, let’s add a simple guard.

fun printShoppingList(items: List<String>) {
    if (items.isEmpty()) {
        println("Shopping list is empty.") // Shopping list is empty.
        return
    }

    println("Shopping list:")
    for (i in 0..items.lastIndex) {
        println("${i + 1}. ${items[i]}")
    }
}

Adding an item: add(...) and trim()

Now let’s write a function that reads a line, trims whitespace, and adds it to the list. Here we use the familiar pattern “read → prepare → use”.

fun addItem(items: MutableList<String>) {
    print("Enter an item: ")
    val raw = readln()
    val name = raw.trim()

    if (name.isEmpty()) {
        println("Empty name — not adding.") // Empty name — not adding.
        return
    }

    items.add(name)
    println("Added: $name") // for example: Added: milk
}

Deleting by number: removeAt(index) and converting “number → index”

Now let’s implement deletion by the number the user enters (1, 2, 3, …). We need to convert the number to an index: index = number - 1. And we must check bounds; otherwise, hello runtime error.

fun removeItemByNumber(items: MutableList<String>) {
    print("Enter the item number to remove: ")
    val number = readln().trim().toIntOrNull()

    if (number == null) {
        println("That is not a number.") // That is not a number.
        return
    }

    val index = number - 1
    if (index < 0 || index > items.lastIndex) {
        println("No item with number $number.") // No item with number 10.
        return
    }

    val removed = items.removeAt(index)
    println("Removed: $removed") // for example: Removed: milk
}

Mini-main: just to feel that the list is alive

For now, we’re not building a full command menu (it easily turns into a “tiny operating system”), but we’ll do 3 steps: add two items, print, remove by number, print again.

fun main() {
    val items = mutableListOf<String>()

    addItem(items)
    addItem(items)
    printShoppingList(items)

    removeItemByNumber(items)
    printShoppingList(items)
}

If you run this and try some input, you’ll feel the main point: MutableList is a container that’s convenient to mutate, and the add/removeAt operations do exactly what they say on the box.

5. Useful nuances and a cheat sheet

val for MutableList is fine

At this stage many people make a logical (and very human) mistake: “If the list changes, then the variable must be var.” In reality, most of the time the variable should be val, because you’re not going to reassign the reference to a different list — you’re going to change the contents of the current list.

An example of “the right kind of normal”:

fun main() {
    val items = mutableListOf("a", "b")

    items.add("c")
    items[0] = "A"

    println(items) // [A, b, c]
}

val prevents doing this:

items = mutableListOf("x") // not allowed, but we usually don't need it

But it doesn’t prevent calling add, removeAt, remove, or modifying elements. It’s like “the house address is fixed, but you can rearrange the furniture inside”.

Cheat-sheet table for MutableList operations

Sometimes it’s useful to have a compact cheat sheet so you don’t confuse methods (especially remove and removeAt — they love to sneak up at night and swap meanings).

Operation Example What it does What it returns
Read
x = list[i]
Gets the element by index
T
Replace
list[i] = v
Replaces the element by index
Unit
Add
list.add(v)
Adds to the end
Boolean
Remove by index
list.removeAt(i)
Removes the element at position
i
the removed
T
Remove by value
list.remove(v)
Removes the first matching
v
Boolean
Check membership
v in list
Whether there is at least one
v
Boolean

6. Typical mistakes when working with List and MutableList

Mistake #1: mixing up size and lastIndex.
It’s very tempting to write list[list.size] and expect “the last element”, because “size is about the end”. But size is the number of elements, and indices go from 0 to size - 1 (i.e., lastIndex). Avoiding the mistake is simple: “the last element” is list[list.lastIndex], not list[list.size].

Mistake #2: trying to modify a List created via listOf.
listOf(...) gives a read-only List. It’s perfect for a “list of constants” or a “temporary set of values”, but if you then want add/remove, you need mutableListOf(...) and the MutableList type.

Mistake #3: confusing removeAt(index) and remove(value).
This is the classic “grabbed the wrong key” level. removeAt(2) removes the element at position 2, while remove("2") (if you wrote it like that) would try to remove the value "2". A good habit: if the name contains At, it’s “by location”, i.e., by index.

Mistake #4: forgetting that after removeAt elements shift.
Beginners sometimes save an index, delete an element, and then try to access the “next” element using the old index, being surprised by strange behavior. After deletion, the list changes: all elements to the right “move” to an index one less, and that’s normal. If you write code that deletes multiple elements in a row, you must remember this shift and act carefully.

Mistake #5: assuming remove(value) will remove all matches.
When the list has duplicates, remove(value) removes only the first match. This matches the meaning of a list: order matters, and duplicates are allowed. If you expected “remove all”, then you expected a different operation (but we’re not introducing it today — first we’ll learn to confidently control the basic mechanics).

Mistake #6: declaring var items = mutableListOf(...) “because the list changes”.
Usually val is better: you’re changing the contents, not switching the variable to a new list. You need var only if you actually plan to replace the entire list. Defaulting to val makes code calmer: fewer places where “suddenly everything got reassigned”, and fewer chances to lose data.

1
Task
Kotlin SELF,  19 level,  1 lecture
Locked
Language passport
Language passport
1
Task
Kotlin SELF,  19 level,  1 lecture
Locked
Casting applicants
Casting applicants
1
Task
Kotlin SELF,  19 level,  1 lecture
Locked
Correcting grades
Correcting grades
1
Task
Kotlin SELF,  19 level,  1 lecture
Locked
Roster change
Roster change
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet