What "out of bounds" means
A sequence with n elements has valid indices 0 through n - 1. Any other index points at nothing: -1 is before the first element, and n is one past the last. In Scala, asking for such an element does not return a default or null. It throws an exception, and a program that doesn't catch it stops.
This lesson uses an ArrayBuffer, Scala's growable, mutable sequence, written with its full name scala.collection.mutable.ArrayBuffer because it isn't imported by default. Everything below applies equally to Array, List and Vector, which all use parentheses for indexing.
What the error looks like
For a buffer holding four elements, index 4 and index -1 are both invalid:
val ages = scala.collection.mutable.ArrayBuffer(14, 32, 50, 21)
println(ages(0)) // 14
println(ages(3)) // 21
println(ages(4)) // throws
println(ages(-1)) // throws
The two failing lines stop the program with these messages:
java.lang.IndexOutOfBoundsException: 4 is out of bounds (min 0, max 3)
java.lang.IndexOutOfBoundsException: -1 is out of bounds (min 0, max 3)
The message tells you the bad index and the valid range, which is usually enough to find the mistake. A plain Array throws the closely related ArrayIndexOutOfBoundsException, with the text Index 3 out of bounds for length 3 for a three-element array.
The lesson's exercise
The starting program should print the last element of ages, which is 21, but it crashes:
val ages = scala.collection.mutable.ArrayBuffer(14, 32, 50, 21)
println(ages(ages.size))
The buffer has four elements, so ages.size is 4. The valid indices are 0 to 3, which means the program asks for index 4, one past the end. This is the classic off-by-one error: the size is a count, but an index counts from zero, so the last index is always one less than the size.
The fix is to subtract one:
val ages = scala.collection.mutable.ArrayBuffer(14, 32, 50, 21)
println(ages(ages.size - 1))
Now the index is 3 and the program prints 21. Because the index is computed from size, it keeps pointing at the last element even after the buffer grows or shrinks.
Guarding against bad indices
Subtracting one is not enough when the sequence might be empty. For an empty buffer, size - 1 is -1, which is itself invalid:
java.lang.IndexOutOfBoundsException: -1 is out of bounds (min 0, max -1)
Scala offers safer ways to read an element that might not exist:
ages.lift(10)returns anOption:Some(element)for a valid index andNonefor an invalid one, so nothing is thrown. For example,ages.lift(1)isSome(32)andages.lift(10)isNone.ages.lastOptionandages.headOptiongive the last and first element as anOption. On an empty buffer they areNone.- An explicit check,
if (i >= 0 && i < ages.size), works when you need to run different code for the two cases.
val ages = scala.collection.mutable.ArrayBuffer(14, 32, 50, 21)
println(ages.lift(1)) // Some(32)
println(ages.lift(10)) // None
println(ages.lastOption) // Some(21)
Common mistakes
- Using
sizeas an index. It is one past the last valid index. - Looping one step too far. A loop such as
for (i <- 0 to ages.size)includesages.sizeand fails on the last pass. Use0 until ages.size, which stops before the end. - Negative indices. Scala does not count from the end the way some languages do.
ages(-1)is an error, not the last element. - Assuming the buffer is not empty. Check the size, or use
lastOption, when the data may be empty.
Key takeaways
- Valid indices run from
0tosize - 1. - An invalid index throws
IndexOutOfBoundsException; nothing is returned and nothing is silently skipped. - The last element is
ages(ages.size - 1). liftandlastOptionturn a missing element intoNoneinstead of a crash.
