Control Flow & Ranges
for over ranges and collections, ranges as first-class values (1..10, until, downTo, step), and why Kotlin drops the C-style for loop for something more readable.
On this page
Loops and iteration read more cleanly in Kotlin, largely because of one feature: ranges are first-class
values. The C-style for (int i = 0; i < n; i++) loop - a fixture of Java since forever - is gone, replaced by
iterating over a range or a collection directly. It's more readable, harder to get wrong (no off-by-one from a
misplaced <=), and ranges turn out to be useful well beyond loops.
for iterates over things, not counters
Kotlin's for loop always iterates over something iterable - a range, a collection, an array. There's no
three-part counter loop:
for (i in 1..5) println(i) // 1 2 3 4 5 - iterate a range
for (name in names) println(name) // iterate a collection directly
for (c in "hello") println(c) // iterate a string's charactersTo loop with an index over a collection, you use helpers rather than a manual counter:
for (i in names.indices) println(names[i]) // indices gives the valid index range
for ((i, name) in names.withIndex()) { // both index and value, destructured
println("$i: $name")
}This eliminates the classic off-by-one bugs of for (int i = 0; i <= n; i++) (should it be < or <=?) - you
name the range you mean, and the loop can't run past it.
Ranges are first-class
A range like 1..5 is a real object (IntRange), and Kotlin gives you a small vocabulary to build exactly the
sequence you want:
1..5 // 1, 2, 3, 4, 5 (inclusive on both ends)
1..<5 // 1, 2, 3, 4 (exclusive end; older syntax: 1 until 5)
5 downTo 1 // 5, 4, 3, 2, 1 (descending)
1..10 step 2 // 1, 3, 5, 7, 9 (custom step)
'a'..'z' // a range of charactersBecause a range is a value, it's not just for loops - you use it in when (in 4..10, from the expressions
lesson) and in membership checks:
if (age in 18..65) { ... } // readable membership test
if (index !in list.indices) return // "not in" - out of bounds check
val isDigit = c in '0'..'9' // instead of c >= '0' && c <= '9'The in operator (backed by ranges) turns clunky x >= lo && x <= hi comparisons into readable x in lo..hi -
a small but constant improvement in intent-revealing code.
while and do-while (unchanged)
Not everything is different. while and do-while work exactly as in Java, for the cases where you genuinely
need a condition-driven loop rather than iteration:
while (queue.isNotEmpty()) { process(queue.poll()) }Kotlin keeps these as-is because they're already fine - the redesign is targeted at the error-prone C-style for,
not iteration in general. (Kotlin also has break and continue, plus labels like outer@ for breaking out
of nested loops, if you need them - rare, but available.)
Often you don't need an explicit loop at all
Ranges and for-loops are the imperative option, but idiomatic Kotlin frequently replaces loops with functional
collection operations (map, filter, forEach, sum) - the subject of the collections module. (1..100).sum(),
names.forEach { println(it) }, or list.filter { it > 0 }.map { it * 2 } often read better than an explicit loop.
Reach for a range-based for when you genuinely need imperative iteration; otherwise the collection functions are
usually cleaner. Both are available - pick the clearer one.
Java's C-style for loop is driving yourself while watching the odometer: 'start at mile 0, keep going while under
mile 5, add one each time' - flexible, but you can misread the gauge and overshoot (the < vs <= off-by-one).
Kotlin's for-in-range is a guided tour with a fixed itinerary: 'visit stops 1 through 5' - you name the
destinations and simply can't drive past the last one. Ranges being first-class means that same itinerary is
reusable off the road too: you can ask 'is this address on the tour route?' (age in 18..65) without driving it.
And when you don't even need to visit each stop yourself, you hand the whole route to a service that processes it
for you (map/filter) - you only take the wheel (an explicit loop) when the journey genuinely needs manual control.
Convert to idiomatic Kotlin: (1) a Java for (int i = 0; i < items.size(); i++) that prints each item with its
index; (2) a countdown for (int i = 10; i >= 1; i--); (3) a Java check if (score >= 0 && score <= 100). Use
ranges and the appropriate helpers, and note one bug class this style prevents.
Why does Kotlin replace Java's C-style for loop with for-in over ranges and collections?
Key takeaways
- Kotlin's for loop iterates over something (a range, collection, or string) - there's no C-style counter loop, which eliminates off-by-one boundary bugs.
- For an index, use collection.indices or withIndex() (destructured to (index, value)) rather than a manual counter.
- Ranges are first-class values: 1..5 (inclusive), 1..<5 / until (exclusive end), downTo (descending), step (custom stride), and character ranges.
- Because ranges are values, the 'in' operator gives readable membership tests (age in 18..65, c in '0'..'9') replacing clunky >= && <= comparisons.
- while/do-while are unchanged; and idiomatic Kotlin often replaces explicit loops entirely with functional collection operations (map/filter/forEach).