JavaRush/Courses/Kotlin SELF/Iterating a List: indices, lastIndex, withIndex() and cho...

Iterating a List: indices, lastIndex, withIndex() and choosing a style

Open

1. Indices in List: when you need them

When you first learn that List supports index-based access, you want to write something like: “I’m a programmer, so I’ll iterate by indices.” I get it. I had that feeling too—as if otherwise the IDE would be offended. But in Kotlin, index-based iteration is a tool, not a lifestyle.

List is an ordered collection: elements are stored in a specific order, and you can access them by position. Indices start at 0, and the last valid index is lastIndex, which equals size - 1. This gives us two ways of thinking: “iterate by values” (usually simpler) and “iterate by positions” (needed only when the position truly participates in the logic).

Let’s agree on a practical rule right away: if you don’t need the index—don’t take the index. The code will be shorter, calmer, and less eager to crash.

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

    for (p in products) {
        println(p)
    }
    // milk
    // bread
    // tea
}

Iterating by values — the “default mode”

In Kotlin, the language designers are pretty clearly hinting: “if you can write it simply—write it simply.” And for (item in list) is exactly that case. This works because List is an Iterable, which means it has an iterator that for uses implicitly.

This style is great because it’s almost impossible to make a boundary mistake. No i + 1, no size - 1, no “oops, I accidentally wrote <= instead of <.” You iterate over the elements, period.

Suppose in a practical mini app we have a list of tasks (no classes yet, just strings):

fun main() {
    val tasks = listOf("pay rent", "buy tea", "learn Kotlin")

    for (task in tasks) {
        println("TODO: $task")
    }
    // TODO: pay rent
    // TODO: buy tea
    // TODO: learn Kotlin
}

Reads perfectly. But sometimes we still need the index: for example, to print “1. … 2. … 3. …”, or to access a “neighboring” element, or to delete by number (deletion is a separate story, but we already need numbering). That’s where indices, lastIndex, and withIndex() come in.

size, lastIndex, and an empty list: where boundary errors begin

Lists have two values that beginners constantly mix up: size and lastIndex. Typically it goes like this: “if the size is 3, then the indices are 0..3.” And this is where Kotlin, like a strict teacher, pulls out the grade book and gives you an IndexOutOfBoundsException.

The correct picture of the world is: size is the number of elements, and lastIndex is the last valid index, and it equals size - 1. So if there are 3 elements, the indices are 0, 1, 2.

It gets especially interesting when the list is empty. Then size = 0, and lastIndex = -1. That sounds weird (“minus first element?”), but in practice it’s convenient: many ranges in Kotlin become empty with those bounds, and the loop simply doesn’t run—without an extra check.

Here’s a mini table that’s useful to keep in your head (not as a mantra, but as a “common-sense cheat sheet”):

List size lastIndex Valid indices
["a", "b", "c"]
3
2
0..2
["only"]
1
0
0..0
[]
0
-1
none

Now let’s show the typical “confused size with lastIndex” mistake:

fun main() {
    val items = listOf("A", "B", "C")

    // BAD: i will go 0,1,2,3 — but index 3 no longer exists
    for (i in 0..items.size) {
        println(items[i])
    }
}

This code will crash when i == 3, because items[3] doesn’t exist. The fix is to use correct bounds, and that’s exactly where indices appears.

indices: safe bounds for iterating by indices

When you iterate a list by indices, your real task is to choose a range of valid indices. Kotlin gives you a ready-made solution: the indices property, which returns the range of correct indices, and you can use it directly in for.

The key convenience of indices is that it automatically works correctly for an empty list and for a list of any size. You don’t think about 0..lastIndex; you simply say: “iterate over valid indices.”

fun main() {
    val items = listOf("milk", "bread", "tea")

    for (i in items.indices) {
        println("$i -> ${items[i]}")
    }
    // 0 -> milk
    // 1 -> bread
    // 2 -> tea
}

Here’s the important point: if you’re writing items[i], you must be sure that i is valid. indices is precisely “I guarantee correctness.”

This style is needed when the index participates in the logic. For example, you want to look at the “current and next element” (the simplest example is building pairs). That’s harder without indices:

fun main() {
    val items = listOf("A", "B", "C")

    for (i in 0 until items.lastIndex) {
        val left = items[i]
        val right = items[i + 1]
        println("$left + $right")
    }
    // A + B
    // B + C
}

Notice: here I used items.lastIndex as the boundary (otherwise i + 1 can go out of bounds). The very fact that you’re writing i + 1 is already a signal: “in this loop you have to think about boundaries.” That’s a normal price for more complex logic.

If until feels “scary” (or you still don’t have a good feel for it), you can write it like this—but carefully:

fun main() {
    val items = listOf("A", "B", "C")

    for (i in items.indices) {
        if (i == items.lastIndex) continue
        println("${items[i]} + ${items[i + 1]}")
    }
    // A + B
    // B + C
}

Yes, we added an if, but we stay within indices and explicitly protect ourselves from the last element.

2. Index vs. number: don’t confuse internal and external

A number for a human and an index for Kotlin are different things

When you make a console app, you quickly find out that users (and instructors grading your work) love 1-based numbering. Kotlin, like a typical engineer, starts from zero. And if you don’t settle with yourself who’s in charge, you’ll keep deleting “the wrong task.”

The idea is simple: an index is the internal address of an element in the list (0..lastIndex), and a number is an external, human-readable identifier (1..size). Almost always, if you print a list, you print the number, not the index.

Here is an “index-based” output that looks a bit sad:

fun main() {
    val tasks = listOf("pay rent", "buy tea")

    for (i in tasks.indices) {
        println("$i. ${tasks[i]}")
    }
    // 0. pay rent
    // 1. buy tea
}

And here is the “human” output—just add + 1:

fun main() {
    val tasks = listOf("pay rent", "buy tea")

    for (i in tasks.indices) {
        println("${i + 1}. ${tasks[i]}")
    }
    // 1. pay rent
    // 2. buy tea
}

It looks like a small thing, but for commands like remove 2 it’s a matter of the program surviving without drama. Remember this rule: input is a “number,” storage is an “index,” conversion is “number - 1.”

withIndex(): index + value without manual list[i]

A loop over indices works, but it has one small issue: you keep seeing tasks[i], and the code turns into “a lot of square brackets.” In Kotlin, for more readable “index + value” iteration, there’s withIndex()—a standard function that lets you iterate pairs “index and element” right in the for.

The nicest bonus of withIndex() is that you can use destructuring: for ((index, value) in list.withIndex()). And the code becomes conversational: “for each index and value…”.

fun main() {
    val tasks = listOf("pay rent", "buy tea", "learn Kotlin")

    for ((index, task) in tasks.withIndex()) {
        println("${index + 1}. $task")
    }
    // 1. pay rent
    // 2. buy tea
    // 3. learn Kotlin
}

Technically, withIndex() returns an object you can iterate, and inside it holds elements with indices. But for now, what matters is practice: this style is more readable when you need both.

One more note: withIndex() looks especially nice when the names are meaningful. If you write for ((i, x) in ...)—that’s still tolerable. But if you write for ((a, b) in ...) and then do a + 1 and print b, the person next to you may start suspecting you’re encrypting a message to an alien civilization.

3. How to choose an iteration style

When there are three ways to iterate, a beginner starts thinking they need to choose “the most correct one” once and for all. Relax: there isn’t a single correct one. There’s the one that fits the situation. The good news is that the selection criteria are very simple, and you can turn them into a mini algorithm for your brain.

Imagine you’re writing code and asking yourself one question: “Do I need an index?” If not—iterate by values. If yes—ask the second: “Do I need the index for accessing neighbors/positions, or just for printing a number?” If it’s just printing—most often withIndex() is more convenient. If it’s positional logic—use indices.

You can depict it as this flowchart:

flowchart TD
    A["Need to iterate over a List"] --> B{"Need an index?"}
    B -->|No| C["for (x in list)"]
    B -->|Yes| D{"Why do you need the index?"}
    D -->|Only number/printing| E["for ((i, x) in list.withIndex())"]
    D -->|Positional logic: neighbors, i+1, i-1| F["for (i in list.indices) { ... list[i] ... }"]

The point isn’t to worship the diagram; it’s to avoid doing an “index-based loop out of habit.” Kotlin reads really well when you write the minimum necessary.

4. Mini project: the list command in Task CLI

Now we’ll carefully apply the new material in our little app. Let it be the simplest “Task CLI”: we store tasks in a MutableList<String>, add commands, and the most important command at this stage is to show the list nicely. If the list is displayed poorly, the user has nothing to type into remove later, and they get upset (and an upset user is scarier than the compiler).

Let’s start with a print function. Note: I take a List<String>, not a MutableList<String>, because printing should not modify the list. This is a good habit: “take the narrowest type you need for the job.”

fun printTasks(tasks: List<String>) {
    if (tasks.isEmpty()) {
        println("Task list is empty.")
        return
    }

    for ((index, task) in tasks.withIndex()) {
        println("${index + 1}. $task")
    }
}

Now an example main that demonstrates it:

fun main() {
    val tasks = mutableListOf<String>()
    tasks.add("pay rent")
    tasks.add("buy tea")

    printTasks(tasks)
    // 1. pay rent
    // 2. buy tea
}

And now a small but important detail: sometimes you want to print not only the task but also some internal information tied to the index. For example, a hint like “internal index” (for debugging) or a format like “#0001”. For that format, withIndex() is also perfect—you already get the index anyway.

fun main() {
    val tasks = listOf("pay rent", "buy tea")

    for ((index, task) in tasks.withIndex()) {
        val debugIndex = index
        println("[$debugIndex] ${index + 1}. $task")
    }
    // [0] 1. pay rent
    // [1] 2. buy tea
}

In a real app you obviously won’t print [0], but as an educational “X-ray” it’s useful: you literally see the difference between “index vs. number.”

5. Common mistakes when iterating a List

Mistake No. 1: using size as the last index.
This is the most common and the most annoying mistake, because it looks logical: “if there are size elements, then the last index is size.” But no: indices start at 0, so the last valid index is lastIndex, i.e., size - 1. The fix is simple: in index-based loops use indices or explicitly lastIndex, not size.

Mistake No. 2: printing indices to the user, but taking numbers from them (or vice versa).
If you output 0. … and the user types remove 1, you’ll inevitably get “the wrong element” and a lot of confusion. The solution is to agree: show the user numbering from 1, and inside the program work with indices from 0. And always convert index = number - 1.

Mistake No. 3: using index-based iteration “on autopilot” when the index isn’t needed anywhere.
Code like for (i in tasks.indices) println(tasks[i]) isn’t broken, but it’s heavier to read than for (task in tasks) println(task). If the index doesn’t participate in the logic, it’s better to iterate by values: fewer chances to accidentally add “dangerous index math” and get a boundary error.

Mistake No. 4: trying to pack too much logic into withIndex() and losing meaning.
withIndex() is great when you print “number + value” or do a small position tie-in. But if inside the loop you start complex arithmetic, neighbor checks, and multiple temporary variables, it’s often easier to switch to indices, where you explicitly control access via list[i] and can neatly handle i - 1 and i + 1.

Mistake No. 5: not thinking about an empty list in user-facing output.
Technically, for (i in list.indices) on an empty list simply won’t run, and that’s safe. But the UX becomes weird: the user typed list, and the program “silently said nothing.” So for a command-line app it’s better to explicitly handle isEmpty() and print a clear message like “The list is empty.”

1
Task
Kotlin SELF,  20 level,  1 lecture
Locked
Grocery shelf
Grocery shelf
1
Task
Kotlin SELF,  20 level,  1 lecture
Locked
Task Queue
Task Queue
1
Task
Kotlin SELF,  20 level,  1 lecture
Locked
Find a string
Find a string
1
Task
Kotlin SELF,  20 level,  1 lecture
Locked
Adjacent sums
Adjacent sums
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet