Why add and remove use different bounds checks
Try This First
You have a linked list containing [10, 20, 30] (size 3). Which of these calls is valid?
list.add(3, 40)list.remove(3)
Think about it, then reveal the answer.
Reveal
list.add(3, 40) is valid. Inserting at index 3 means appending after the last element, giving [10, 20, 30, 40]. The valid range for add is 0 through size inclusive.
list.remove(3) is not valid. There is no element at index 3 (indices run 0, 1, 2). The call throws IndexOutOfBoundsException. The valid range for remove is 0 through size - 1 inclusive.
What You Need To Walk In With
add(int i, T x)accepts0 <= i <= size. The upper limit issizebecause inserting at positionsizeis the same as appending to the end.remove(int i)accepts0 <= i < size. The upper limit issize - 1becauseremovemust point at an element that actually exists.- These two ranges differ by exactly one:
addallowsi == size,removedoes not. - Both reject negative indices. A negative index has no meaning in a sequential list.
- The exception type for an invalid index is
IndexOutOfBoundsException(Liang 10e Ch 24).
How It Works
The key insight is what each operation needs at position i:
addneeds a place to insert. Placing a new node before the current node ati(or at the end wheni == size) is always valid as long asifalls in[0, size].removeneeds an existing node to delete. There is no node at indexsize, so that index is always out of range.
Here are the two guard clauses for a singly linked list with a head reference and a size field:
// In add(int i, T x)
if (i < 0 || i > size) { // i == size is VALID for add
throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size);
}
// In remove(int i)
if (i < 0 || i >= size) { // i == size is INVALID for remove
throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size);
}
Notice the one character that differs: > in add, >= in remove.
A minimal sketch for add on [a, b, c] at index 3 (size = 3):
Before: head -> [a] -> [b] -> [c] -> null
add(3, "d"): traverse to node at index 2 (the last node),
attach new node after it.
After: head -> [a] -> [b] -> [c] -> [d] -> null
size becomes 4.
For remove(3) on the same list, the guard fires immediately because 3 >= 3 (size is 3), and no traversal happens.
Worked Example: Predict, Then Check
A list holds [x, y, z] (size 3). Trace these two calls in order:
list.add(3, "w"); // call A
list.remove(4); // call B
After call A, what is the list state and size? After call B, what happens?
Reveal
Call A: i = 3, size = 3. The guard checks 3 < 0 || 3 > 3, which is false (3 is not greater than 3). The call is valid. "w" is appended: list is [x, y, z, w], size becomes 4.
Call B: i = 4, size = 4. The guard checks 4 < 0 || 4 >= 4, which is true (4 is equal to 4). IndexOutOfBoundsException is thrown. The list remains [x, y, z, w].
A Common Mistake
When implementing add, developers sometimes copy the guard from remove without changing it:
// Wrong: copied from remove
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException(...);
}
On a list of size 5, this rejects i = 5 even though appending to the end is perfectly valid. The method throws where it should insert. The fix is simple: change >= to > so that i == size passes the guard. (Source: Liang 10e Ch 24)
Go Deeper (optional)
Both add and remove at an arbitrary index cost O(n) time because the guard check is O(1) but the traversal to position i is O(n). If you later study a doubly linked list with both a head and a tail reference, the bounds logic stays identical: the index range asymmetry between add and remove is not a property of the node structure, it is a property of what each operation means.
Check Yourself
Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.
Check your understanding
For a list of size 7, which index range is valid for add(int i, T x)?
For the same list of size 7, which index range is valid for remove(int i)?
list = [p, q, r] (size 3). What is the result of list.add(3, “s”)?
list = [p, q, r] (size 3). What is the result of list.remove(3)?
A developer writes the add guard as: if (i < 0 || i >= size). What bug does this introduce?