article banner

The power of Kotlin for-loop

This is a chapter from the book Kotlin Essentials. You can find it on LeanPub or Amazon. It is also available as a course.

In Java and other older languages, a for-loop typically has three parts: the first initializes the variable before the loop starts; the second contains the condition for the execution of the code block; the third is executed after the code block.

// Java
for(int i=0; i < 5; i++){
   System.out.println(i);
}

However, this is considered complicated and error-prone. Just consider a situation in which someone uses > or <= instead of <. Such a small difference is not easy to notice, but it essentially influences the behavior of this for-loop.

As an alternative to this classic for-loop, many languages have introduced a modern alternative for iterating over collections. This is why, in languages like Java or JavaScript, there are two completely different kinds of for-loops, both of which are defined with the same for keyword. Kotlin has simplified this. In Kotlin, we have one universal for-loop that can be expressively used to iterate over a collection, a map, a range of numbers, and much more.

In general, a for-loop is used in Kotlin to iterate over something that is iterable1.

We can iterate over lists or sets.

fun main() { val list = listOf("A", "B", "C") for (letter in list) { print(letter) } // Variable type can be explicit for (str: String in setOf("D", "E", "F")) { print(str) } } // ABCDEF

We can also iterate over any other object as long as it contains the iterator method with no parameters, plus the Iterator result type and the operator modifier. The easiest way to define this method is to make your class implement the Iterable interface.

fun main() { val tree = Tree( value = "B", left = Tree("A"), right = Tree("D", left = Tree("C")) ) for (value in tree) { print(value) // ABCD } } class Tree( val value: String, val left: Tree? = null, val right: Tree? = null, ) : Iterable<String> { override fun iterator(): Iterator<String> = iterator { if (left != null) yieldAll(left) yield(value) if (right != null) yieldAll(right) } }

The inferred variable type of the variable defined inside the for-loop comes from the Iterable type argument. When we iterate over Iterable<User>, the inferred element type will be User. When we iterate over Iterable<Long?>, the inferred element type will be Long?. The same applies to all other types.

This mechanism, which relies on Iterable, is really powerful and allows us to cover numerous use cases, one of the most notable of which is the use of ranges to express progressions.

Ranges

In Kotlin, if you place two dots between two numbers, like 1..5, you create an IntRange. This class implements Iterable<Int>, so we can use it in a for-loop:

fun main() { for (i in 1..5) { print(i) } } // 12345

This solution is efficient as well as convenient because the Kotlin compiler optimizes its performance under the hood.

Ranges created with .. include the last value (which means they are closed ranges). If you want a range that stops before the last value, use the ..< operator or until infix function instead.

fun main() { for (i in 1..<5) { print(i) } } // 1234
fun main() { for (i in 1 until 5) { print(i) } } // 1234

Both .. and ..< start with the value on their left and progress toward the right number in increments of one. If you use a bigger number on the left, the result is an empty range.

fun main() { for (i in 5..1) { print(i) } for (i in 5..<1) { print(i) } } // (nothing is printed)

If you want to iterate in the other direction, from larger to smaller numbers, use the downTo function.

fun main() { for (i in 5 downTo 1) { print(i) } } // 54321

The default step in all those cases is 1. If you want to use a different step, you should use the step infix function.

fun main() { for (i in 1..10 step 3) { print("$i ") } println() for (i in 1..<10 step 3) { print("$i ") } println() for (i in 10 downTo 1 step 3) { print("$i ") } } // 1 4 7 10 // 1 4 7 // 10 7 4 1

Break and continue

Inside loops, we can use the break and continue keywords:

  • break - terminates the nearest enclosing loop.
  • continue - proceeds to the next step of the nearest enclosing loop.
fun main() { for (i in 1..5) { if (i == 3) break print(i) } // 12 println() for (i in 1..5) { if (i == 3) continue print(i) } // 1245 }

Both are used rather rarely, and I had trouble finding even one real-life example in the commercial projects I have co-created. I also assume that they are well-known to developers who’ve come to Kotlin from older languages. This is why I present these keywords so briefly.

Use cases

Developers with experience in older languages often use a for-loop where slightly more-modern alternatives should be used instead. For instance, in some projects I can find a for-loop that is used to iterate over elements with indices.

fun main() { val names = listOf("Alex", "Bob", "Celina") for (i in 0..<names.size) { val name = names[i] println("[$i] $name") } } // [0] Alex // [1] Bob // [2] Celina

This is not a good solution. There are multiple ways to do this better in Kotlin.

First, instead of explicitly iterating over a range 0..<names.size, we could use the indices property, which returns a range of available indices.

fun main() { val names = listOf("Alex", "Bob", "Celina") for (i in names.indices) { val name = names[i] println("[$i] $name") } } // [0] Alex // [1] Bob // [2] Celina

Second, instead of iterating over indices and finding an element for each of them, we could instead iterate over indexed values. We can create indexed values using withIndex on iterable. Each indexed value includes both an index and a value. Such objects can be destructured in a for-loop2.

fun main() { val names = listOf("Alex", "Bob", "Celina") for ((i, name) in names.withIndex()) { println("[$i] $name") } } // [0] Alex // [1] Bob // [2] Celina

Third, an even better solution is to use forEachIndexed, which is explained in the next book: Functional Kotlin.

fun main() { val names = listOf("Alex", "Bob", "Celina") names.forEachIndexed { i, name -> println("[$i] $name") } } // [0] Alex // [1] Bob // [2] Celina

Another popular use case is iterating over a map. Developers with a Java background often do it this way:

fun main() { val capitals = mapOf( "USA" to "Washington DC", "Poland" to "Warsaw", "Ukraine" to "Kiev" ) for (entry in capitals.entries) { println("The capital of ${entry.key} is ${entry.value}") } } // The capital of USA is Washington DC // The capital of Poland is Warsaw // The capital of Ukraine is Kiev

This can be improved by directly iterating over a map, so calling entries is unnecessary. Also, we can destructure entries to better name the values.

fun main() { val capitals = mapOf( "USA" to "Washington DC", "Poland" to "Warsaw", "Ukraine" to "Kiev" ) for ((country, capital) in capitals) { println("The capital of $country is $capital") } } // The capital of USA is Washington DC // The capital of Poland is Warsaw // The capital of Ukraine is Kiev

We can use forEach for a map.

fun main() { val capitals = mapOf( "USA" to "Washington DC", "Poland" to "Warsaw", "Ukraine" to "Kiev" ) capitals.forEach { (country, capital) -> println("The capital of $country is $capital") } } // The capital of USA is Washington DC // The capital of Poland is Warsaw // The capital of Ukraine is Kiev

Summary

In this chapter, we've learned about using the for-loop. It is really simple and powerful in Kotlin, so it’s worth knowing how it works, even though it’s not used very often (due to Kotlin’s amazing functional features, which are often used instead).

Now, let's talk about one of the most important Kotlin improvements over Java: good support for handling nullability.

1:

Has the iterator operator method.

2:

Destructuring will be explained in more depth in the Data classes chapter.