🗃️ Collections Framework · Intermediate

List operations & pitfalls in Java

remove(int) vs remove(Object), subList views, indexOf.

🧩 The mysteryYou have [1, 2, 3] and call 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 value
🔮 Predict it

Your 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);
  1. [2, 3]
  2. [1, 3]
  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");      // -1

subList 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]
🔮 Predict it

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. [1, 2, 3, 4, 5]
  2. [1, 5]
  3. [2, 3, 4]
  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 trap

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(); // ConcurrentModificationException
💼 In the real world

A 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

  1. remove(1) removes the element AT index 1; remove(Integer.valueOf(1)) removes the value 1
  2. indexOf / lastIndexOf return -1 when not found
  3. subList(from, to): from inclusive, to exclusive, backed by the original
  4. Structurally changing the original invalidates an existing subList
🤯 Did you know?

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. 1 3 -1
  2. 1 3 null
  3. 2 4 -1
  4. 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);
  1. [20, 30]
  2. [10, 30]
  3. [10, 20, 30]
  4. 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.

Next: three Sets all reject duplicates. So why do they print the same elements in three different orders?