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.
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 operator method is to make your class implement the Iterable
interface (then you do not need to define operator
modifier yourself, as it is inherited from Iterable
). In the below example, we define a Tree
class that implements Iterable<String>
, so we must override the iterator
method, and we can iterate over an instance of this class.
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:
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.
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.
If you want to iterate in the other direction, from larger to smaller numbers, use the downTo
function.
The default step in all those cases is 1
. If you want to use a different step, you should use the step
infix function.
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.
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.
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.
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.
Third, an even better solution is to use forEachIndexed
, which is explained in the next book: Functional Kotlin.
Another popular use case is iterating over a map. Developers with a Java background often do it this way:
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.
We can use forEach
for a map.
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.
Has the iterator
operator method.
Destructuring will be explained in more depth in the Data classes chapter.