List operations & pitfalls in Java
remove(int) vs remove(Object), subList views, indexOf.
list.remove(1). Do you get [2, 3] or [1, 3]? Many experienced developers guess wrong.Two methods, one name
List has two remove methods: remove(int index) removes the element at a position, and remove(Object o) removes the first element equal to o. With a List<Integer> both look plausible, and Java must pick one.
E remove(int index); // by position
boolean remove(Object o); // by valueYour turn
What does this print?
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
nums.add(3);
nums.remove(1);
System.out.println(nums);[2, 3][1, 3][1, 2]
Show the answer
[1, 3]. The argument 1 is an int, which matches remove(int index) exactly. remove(Object) would need boxing, and an exact match always wins. So the element at index 1 (the value 2) goes.
Forcing the value version
To remove the value, pass an object: Integer.valueOf(20) or a cast (Integer) 20. Now remove(Object) is the match, and it deletes the first element equal to 20.
var nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(Integer.valueOf(20)); // [10, 30]
nums.remove(0); // [30]Finding things
indexOf(x) returns the index of the first element equal to x; lastIndexOf(x) the last. Not found? They return -1, never null, because the return type is int.
var l = List.of("a", "b", "a");
l.indexOf("a"); // 0
l.lastIndexOf("a"); // 2
l.indexOf("z"); // -1subList is a window, not a copy
subList(from, to) covers indexes from (inclusive) to to (exclusive). It's a live view backed by the original list: changes through the view change the original. That's why list.subList(a, b).clear() is the standard way to delete a range.
var l = new ArrayList<>(List.of(1, 2, 3, 4));
List<Integer> mid = l.subList(1, 3); // [2, 3]Clear the window
What does this print?
var list =
new ArrayList<>(List.of(1, 2, 3, 4, 5));
list.subList(1, 4).clear();
System.out.println(list);[1, 2, 3, 4, 5][1, 5][2, 3, 4][1, 4, 5]
Show the answer
subList(1, 4) is a view of indexes 1, 2 and 3 (values 2, 3, 4). Clearing the view removes them from the original, leaving [1, 5].
The stale window
Create a subList, then structurally change the original (add or remove), and the old view is invalid. Using it throws **ConcurrentModificationException**. Make the subList after you're done changing, or copy it: new ArrayList<>(list.subList(a, b)).
var list = new ArrayList<>(List.of(1, 2, 3));
var sub = list.subList(0, 2);
list.add(9);
sub.size(); // ConcurrentModificationExceptionA classic production bug
Code like ids.remove(userId) with an int userId on a List<Integer> deletes the element at position userId instead of that user, or throws when the id is bigger than the list. It passes casual tests and fails with real data. Reviewers learn to spot it on sight.
Key takeaways
- remove(1) removes the element AT index 1; remove(Integer.valueOf(1)) removes the value 1
- indexOf / lastIndexOf return -1 when not found
- subList(from, to): from inclusive, to exclusive, backed by the original
- Structurally changing the original invalidates an existing subList
The List Javadoc itself recommends list.subList(from, to).clear() to remove a range of elements. There's no separate removeRange in the public List API.
Practice questions
What does this print?
var list = List.of("a", "b", "c", "b");
System.out.println(list.indexOf("b"));
System.out.println(list.lastIndexOf("b"));
System.out.println(list.indexOf("z"));- 1 3 -1
- 1 3 null
- 2 4 -1
- 1 1 -1
Check your answer
1 3 -1. Indexes are 0-based: the first "b" is at 1, the last at 3. A missing element gives -1, never null, because the return type is int.
What does this print?
List<Integer> nums = new ArrayList<>();
nums.add(10);
nums.add(20);
nums.add(30);
nums.remove(1);
System.out.println(nums);- [20, 30]
- [10, 30]
- [10, 20, 30]
- Throws IndexOutOfBoundsException
Check your answer
[10, 30]. remove(1) with an int argument matches remove(int index) exactly, so the element at index 1 (20) is removed.