What goes wrong
A string with n characters has valid indexes 0 to n - 1. Any index outside that range is invalid, whether it is too big or negative. Reading one throws a StringIndexOutOfBoundsException:
val car = "ford"
println(car(0)) // f
println(car(3)) // d
println(car(4)) // throws
println(car.charAt(4)) // throws too
The exception's message names the bad index and the length: Index 4 out of bounds for length 4. If nothing catches it, the program stops at that line, and later lines never run.
The exercise, step by step
The lesson has code meant to print the last character of a message, but it crashes:
val message = "I love pancakes"
println(message(message.length))
- Find the length.
"I love pancakes"has 15 characters, somessage.lengthis 15. - Find the valid indexes. They are 0 to 14. Index 15 is one past the end, which is why the program throws with
Index 15 out of bounds for length 15. - Subtract one. The last character is at
message.length - 1.
val message = "I love pancakes"
println(message(message.length - 1))
This prints s.
Common ways to go out of bounds
- Using
lengthas an index. This is the lesson's bug. Length is a count; the last index is one smaller. - Empty strings.
""(0)fails because no index is valid."".lastfails differently, with aNoSuchElementException. - Negative indexes.
message(-1)throws. Scala does not count from the end the way some languages do. Writemessage(message.length - 1). - Loops that run one step too far. A loop
for (i <- 0 to s.length)includess.lengthand eventually reads past the end. Use0 until s.length, which stops before the end. substringbounds."abc".substring(0, 10)throws too, withRange [0, 10) out of bounds for length 3.
Safe access
When an index might be invalid, check it, or ask for an Option with lift:
val fruit = "Avocado"
println(fruit.lift(2)) // Some(o)
println(fruit.lift(10)) // None
A plain comparison works as well: if (i >= 0 && i < fruit.length) fruit(i). When a bug throws this exception, read the message first. It tells you the index you used and the real length, which usually points straight to the off-by-one.
Catching the exception
You can also handle the error instead of preventing it:
val fruit = "Avocado"
try {
println(fruit(20))
} catch {
case e: StringIndexOutOfBoundsException => println("caught: " + e.getMessage)
}
println("after")
caught: Index 20 out of bounds for length 7
after
Catching is useful for input you don't control. For your own index arithmetic it is better to fix the arithmetic, because an exception hides the real mistake.
Reading the error
When the exception appears in a program, the message has two numbers: the index you asked for and the length of the string. If the index equals the length, you are one step too far, and the fix is usually - 1. If the index is negative, a calculation went below zero. If the length is 0, the string was empty, and you need a check before indexing.
Fixing it for good
After you fix the immediate bug, ask what the code should do with an empty or short string. A function that takes any string from outside should either check the length first or return an Option. The lesson's string always has 15 characters, so the plain fix is enough there. In general, writing the check once, near the place where the index is computed, is easier than finding the crash later.
Key takeaways
- Valid indexes are 0 to
length - 1. - An invalid index throws
StringIndexOutOfBoundsException. - The last character is
s(s.length - 1), nevers(s.length). - Check the index, or use
lift, when it may be invalid.
