Array Index Bounds and Exceptions
A dynamic array lets a collection grow and shrink while the program runs, and the class design hides the internal array behind operations such as add, remove, get, set, and size.
Try it: Indexing and Slicing
How Python indexes a sequence from 0 (or from the end with negative numbers) and how a slice start:stop:step picks positions — with the exact CPython rules for omitted and out-of-range bounds.
How it works
- Positions run 0 … len−1; negative positions count back from the end (−1 is the last).
- An index outside the string raises IndexError.
- A slice resolves start and stop (omitted → the ends; out of range → clamped), then walks by step, stopping before stop.
- A negative step walks backwards; a step of 0 is a ValueError.
Default run (7 steps): 'PYTHONIC'[1:6:2]: a slice copies characters from start up to (not including) stop, moving by step. … 'PYTHONIC'[1:6:2] → 'YHN' (positions 1, 3, 5).
Simplified: Works on a short string (up to 16 printable ASCII characters); lists slice by exactly the same rules.
Loading the simulation…
Try it: try / except / else / finally
How an exception jumps out of a try block to the first matching except clause, when else runs, and why finally always runs.
How it works
- Statements in try run until one raises an exception; the rest of try is skipped.
- Python checks the except clauses in order and runs the first one whose type matches.
- If nothing was raised, the else block runs instead.
- finally runs in every case.
Default run (9 steps): Call safe_divide('4'). … Return 25.0. Output was: Worked: 25.0 / Done.
Simplified: One fixed function; int() and / are modelled exactly (including how Python prints floats), but nothing is executed as Python.
Loading the simulation…
Why a Fixed-Length Array Is Not Enough
Imagine a game that keeps a roster of the players currently taking part in a match. Players join partway through, and leave when they are eliminated. The roster has to reflect those changes at every moment. An ordinary array can hold the players, but its length is fixed when it is created. If you choose a length that is too small, you run out of room. If you choose one that is too large, you waste space and still have to keep track of which slots are actually in use.
A dynamic array solves this by letting the collection itself grow and shrink, so the programmer does not have to guess a size in advance. This article builds a growable collection of ints, a Java class named DynamicArrayOfInt that stores its items in a plain array and wraps that array in operations a caller can use safely. Its central question is what happens when someone asks for a position that does not hold a value.
What do you think happens?
A dynamic array currently holds 5 items at positions 0 through 4. Its internal storage has room for 8 values. If you call get(5), what happens?
Reveal answer
It throws an ArrayIndexOutOfBoundsException
The class counts its items, not its storage slots. Position 5 is at or beyond the item count of 5, so the class rejects it even though the internal array has room there. The rest of this article explains why.
Capacity Versus Size
The class holds two different numbers, and most early bugs come from mixing them up. The first is the length of the internal array, which is the number of slots that exist. The second is the item count, which records how many of those slots currently hold meaningful values. This article calls the first the capacity and the second the size. In the class, the array is stored in a field named items, and the count is stored in a field named itemCt. Because the array is usually only partly full, items.length and itemCt are usually different numbers.
The diagram shows a capacity of 8 and a size of 3. Slots 0 through 2 hold real values, and the remaining five slots are spare room. The class relies on itemCt, not on items.length, to know how much of the array is meaningful. The capacity is the storage that exists; the size is the part of that storage the collection is using.
The Five Operations and Which Ones Change the Count
A useful dynamic array needs a small, fixed set of operations. Each one is an instance method of the class, so a caller writes something like roster.add(7) rather than manipulating the internal array directly. The five essential operations are add, remove, get, set, and size. Their roles are these: add places a new value at the end of the collection, remove deletes the value at a position, get reads the value at a position, set replaces the value at a position, and size reports how many items the collection holds.
Only add and remove change how many items the collection holds. get, set, and size leave the count alone. This split is useful when reading the class: any method that touches itemCt is a method whose effect on the collection's shape you need to think about carefully.
Tracing a Sequence of Calls
The best way to see how these operations interact is to trace them one at a time. Follow the state after each call: the size, the values in the positions that hold items, and the capacity. This example starts with an empty collection whose capacity is 8.
Tracing add, set, remove, get, and size
Starting from an empty DynamicArrayOfInt with capacity 8, perform these calls in order: add(7), add(3), add(9), set(1, 4), remove(0), get(1), get(2), size(). Track the size and the contents after each step, and decide what each get returns.
- 1After add(7), add(3), add(9)
Each add finds spare room, stores its value at position
itemCt, and increasesitemCtby one. The contents are 7, 3, 9 at positions 0, 1, 2, and the size is 3. The capacity is still 8 because the array was never full. - 2After set(1, 4)
set names position 1, which is valid because 1 is less than the size of 3. It replaces the value 3 with 4. The contents are now 7, 4, 9. set does not change the size, so it stays at 3.
- 3After remove(0)
remove deletes the value at position 0. The later values slide one position toward the front, so 4 moves to position 0 and 9 moves to position 1. Then
itemCtdrops from 3 to 2. The contents are 4, 9, and the size is 2. The capacity remains 8, because remove never shrinks the internal array. - 4get(1)
Position 1 is valid because it is less than the size of 2. The value stored there is 9, so
get(1)returns 9. - 5get(2)
Position 2 is not valid now. The valid positions are 0 and 1, because the size is 2. The check fails and the class throws an
ArrayIndexOutOfBoundsException. The slot at position 2 still physically exists in the internal array, but it holds no item. - 6size()
size reports the item count, which is 2. It does not report the capacity of 8.
After the sequence, the collection holds 4 and 9 with size 2 and capacity 8. get(1) returned 9. get(2) threw an ArrayIndexOutOfBoundsException, and size() returns 2.
The before and after columns show what remove does. Every later value moved one position toward the front, and the count dropped by one. The position that used to hold the last value is no longer part of the collection, even though the storage slot is still there.
Why the Valid Range Stops at Size Minus One
Every operation that names a position has to decide what to do with a position that holds no value. The class throws an ArrayIndexOutOfBoundsException. The check is the same in get, set, and remove: the index is rejected if it is negative, or if it is greater than or equal to itemCt. The important detail is the upper limit. Because itemCt counts the items, the valid positions run from 0 through itemCt minus 1.
- 1The lower bound is 0, because positions start at zero.
- 2The upper bound is size minus 1, because the size counts the items and the last item sits one position before the count.
- 3Any index outside this range, whether negative or equal to or greater than the size, causes an
ArrayIndexOutOfBoundsExceptionin get, set, and remove.
With a size of 5, the valid indices are 0, 1, 2, 3, and 4. Index 5 is rejected. The slot at position 5 may physically exist in the internal array, especially when the capacity is larger than the size, but it holds no item. The class checks the count, not the storage length, so that slot is treated as illegal.
How Add Grows the Storage
The class behaves as if it can hold any number of items, but its internal array has a fixed length. The growth happens inside add. Before writing a new value, add checks whether itemCt has reached items.length. If it has, the array is full. The method then asks Arrays.copyOf for a new array with twice the length, copies the existing values into it, and makes items refer to the new array. Only after that does add store the new value at position itemCt and increase itemCt by one.
Because every add that finds a full array doubles the capacity, the storage grows as needed and the caller never has to resize anything. Suppose the capacity is 8 and all 8 slots hold values. A ninth call to add first copies the eight old values into a new array of length 16, places the ninth value at position 8, and sets the size to 9. The eight old values survive the move, the new value lands in the first free position, and the remaining slots of the larger array wait to be used. The doubling rule is the one the class applies; the sizes in this example are illustrative values chosen to show it.
Remove Shifts Values but Keeps the Storage
Removal works in the opposite direction, and it is worth noticing what it does not do. The remove method slides every later value one position toward the front, then decreases itemCt by one. It never shrinks the internal array. The capacity stays what it was, and only the count changes. A dynamic array can therefore grow its storage, but the code in this class does not give that storage back after items are removed.
| Operation | Changes the size | Changes the capacity | Effect on valid range |
|---|---|---|---|
| add | Yes, increases by one | Yes, doubles only when the array is full | Upper limit grows by one |
| remove | Yes, decreases by one | No | Upper limit shrinks by one |
| get | No | No | Position must be below size |
| set | No | No | Position must be below size |
| size | No | No | Reports the current size |
How each operation affects the count, the storage, and the valid index range
Mistakes Beginners Make with Bounds
Common mistake Using the capacity as the upper limit for indices
What it looks likeA collection of size 5 has a capacity of 8, so a learner tries get(7) and expects a value.
Why it failsThe class checks the item count. Positions at or beyond the size hold no item, even if the internal array has room for them.
Do this insteadCompare the index against size, and accept only indices from 0 through size minus 1.
Common mistake Off-by-one error on the upper limit
What it looks likeA loop reads get(size) on the last pass, expecting the final value.
Why it failsThe last item sits at position size minus 1. Position size is the first position beyond the collection.
Do this insteadUse the range 0 through size minus 1, and recheck the boundary when a loop ends.
Common mistake Assuming remove frees the internal storage
What it looks likeAfter removing many items, a learner expects the capacity to drop back toward the size.
Why it failsremove only shifts values and lowers the count. The internal array is never shrunk.
Do this insteadTrack size and capacity separately. Expect capacity to stay the same after removals.
Common mistake Forgetting that set does not change the size, so a learner expects set to add a new item at an index beyond the size
Practice Tracing and Predicting
A DynamicArrayOfInt starts empty with capacity 8. Perform these calls in order: add(4), add(6), add(8), remove(1), add(10). Then state the size, the contents of the valid positions, and whether get(2) would throw an exception.
Hints
- Track the size after every call, because only add and remove change it.
- After
remove(1), the value at position 1 is removed and later values shift toward the front. - The valid positions run from 0 through size minus 1 at the moment of the call.
A dynamic array has size 4 and capacity 8. Without running any code, predict which of these calls throw an ArrayIndexOutOfBoundsException: get(3), get(4), set(-1, 5), and remove(4). Explain your reasoning using the valid range.
Hints
- Valid positions for size 4 are 0 through 3.
- A negative index is always rejected.
- Capacity does not affect the check.
Where ArrayList Fits In
Writing a separate class for every element type would be tedious. A DynamicArrayOfString would be needed for strings, a DynamicArrayOfPlayer for players, and so on. Java avoids that repetition with a built-in standard class, ArrayList, which implements dynamic arrays and can hold any type of data. It follows the same design ideas developed in this article: a growable collection with operations to add, remove, get, and set items.
Key Takeaways
- A dynamic array keeps two numbers: the capacity, which is the internal array length, and the size, which counts the items that hold meaningful values.
- add and remove are the only operations that change the size; add doubles the capacity by copying into a larger array when the storage is full, and remove shifts later values forward without shrinking storage.
- Valid positions run from 0 through size minus 1. Negative indices and indices at or beyond the size cause an
ArrayIndexOutOfBoundsExceptionin get, set, and remove, even when the capacity has room for them. - Tracing each call and recording the size and contents after every step is the reliable way to predict what a sequence of operations produces.