How does AbstractList use Template Method, and what does a minimal immutable List subclass look like?
answer
- AbstractList primitives: get(int) + size()
- Skeletal implementation idiom (Effective Java)
- Override set/add/remove for mutability
- Default add/set/remove throw UnsupportedOperationException
- modCount drives fail-fast iterators
basics
~20 sAbstractList already writes most of the List methods (iterator, indexOf, equals, etc.) using two abstract steps you provide: get(index) and size(). To make a read-only list you extend AbstractList and implement just those two methods.
solid answer
~40 sAbstractList is a 'skeletal implementation' of the List interface. It implements the bulk of the contract — iterator(), listIterator(), indexOf(), lastIndexOf(), equals(), hashCode(), subList() — entirely in terms of two abstract primitive methods: get(int) and size(). That is Template Method at collection scale: the algorithms (iteration order, equality semantics) are the skeleton, and get/size are the gaps. To create a minimal immutable list you extend AbstractList<E>, implement get and size, and inherit everything else correctly. If you also override set, add, and remove you get a mutable list. This is Effective Java's 'skeletal implementation' idiom: the interface (List) declares the type, the abstract class (AbstractList) supplies the heavy lifting, and your class supplies only the data-backed primitives.
code
java · 20 linesimport java.util.AbstractList;
final class IntRangeList extends AbstractList<Integer> {
private final int start, count;
IntRangeList(int start, int count) { this.start = start; this.count = count; }
@Override public Integer get(int index) { // primitive method
if (index < 0 || index >= count)
throw new IndexOutOfBoundsException("" + index);
return start + index;
}
@Override public int size() { return count; } // primitive method
}
// Inherited (skeleton) behavior, all from get + size:
// var r = new IntRangeList(10, 3);
// r.contains(11) -> true
// r.indexOf(12) -> 2
// r.equals(List.of(10,11,12)) -> true
// for (int x : r) ... -> 10, 11, 12go deeper
Knows that extending AbstractList and implementing get and size yields a working read-only list.
Can write the minimal subclass, explains that the inherited methods are built on get/size, and that mutators must be overridden for a mutable list.
Connects it to the 'skeletal implementation' idiom, explains the read-only-by-default mutators, fail-fast modCount, and the equals/hashCode contract reuse.
Evaluates trade-offs of the abstract-class skeleton vs. interface default methods, performance of get-based iteration for non-random-access stores, and when AbstractSequentialList is the better base.
## Background: interface vs. skeletal class In Java, `List<E>` is an **interface** — it declares ~25 methods but (historically) implements none. Implementing all of them by hand for every new list type would be enormous and error-prone. The JDK solves this with **skeletal implementation classes**: `AbstractList`, `AbstractSet`, `AbstractMap`, `AbstractCollection`, `AbstractQueue`. Each pairs with an interface and implements most of it, leaving a tiny set of **abstract primitive methods** for you. ## AbstractList's primitives `AbstractList<E>` declares exactly two abstract methods that a read-only list must supply: - `public abstract E get(int index);` - `public abstract int size();` Everything else is written **once** in terms of these. For example, `AbstractList`'s `iterator()` returns an inner `Itr` whose `next()` calls `get(cursor++)` and whose bounds come from `size()`. `indexOf(Object o)` loops `0..size()` calling `get(i)` and comparing. `equals()` walks two lists element-by-element via their iterators. Because these algorithms only ever touch the world through `get` and `size`, your subclass automatically gets a *correct, contract-compliant* implementation of all of them. This is the **Template Method** pattern: the algorithm skeletons (how to iterate, how to compute equality and hash code, how to search) live in the base class; the variable steps (where the data actually comes from) are the abstract primitives. ## A minimal immutable list ```java import java.util.AbstractList; final class IntRangeList extends AbstractList<Integer> { private final int start, count; IntRangeList(int start, int count) { this.start = start; this.count = count; } @Override public Integer get(int index) { // primitive if (index < 0 || index >= count) throw new IndexOutOfBoundsException("" + index); return start + index; } @Override public int size() { return count; } // primitive } // new IntRangeList(10, 3) behaves as [10, 11, 12]: // .contains(11) -> true, .indexOf(12) -> 2, iteration works, equals to List.of(10,11,12). ``` With only two methods you have a fully functional read-only `List` — `for (int x : list)`, `list.contains(11)`, `list.equals(other)`, and `list.stream()` all work, because each is built on `get`/`size`. ## Making it mutable If you also override: - `set(int index, E element)` → the list becomes *settable* (fixed-size mutable). - `add(int index, E element)` and `remove(int index)` → the list becomes *variable-size*. `AbstractList`'s default versions of these throw `UnsupportedOperationException`, which is exactly why an un-overridden `AbstractList` is read-only. ## The 'modCount' hook `AbstractList` also maintains a protected `modCount` field. Its iterators read it to implement **fail-fast** behavior — if the list is structurally modified during iteration (so `modCount` changes), the iterator throws `ConcurrentModificationException`. Subclasses that support modification are expected to bump `modCount`. This is part of the skeleton you inherit. ## Why this is good design - **Reuse:** dozens of methods written once, shared by every subclass and by the JDK's own `ArrayList`, `Vector`, and the lists returned by `Arrays.asList` and `List.of`. - **Correctness:** equality/hashCode/iteration semantics are guaranteed consistent across all lists. - **Minimal surface for the implementer:** you reason about *data access* (get/size), not algorithm plumbing. This is precisely the *skeletal implementation* recommendation in *Effective Java* (Item 20): provide an interface for the type and an abstract skeletal class for the work, so implementers write the fewest possible methods.
- What happens if you call add() on an AbstractList subclass that only overrode get and size?AbstractList's default add throws UnsupportedOperationException, so the list is effectively read-only until you override the mutator methods.
- Why does AbstractList provide equals() and hashCode() rather than leaving them to subclasses?The List interface specifies exact equals/hashCode semantics (order-sensitive element comparison). Implementing them once in the skeleton guarantees every list obeys the contract and that lists from different classes compare equal correctly.
saying these in an interview costs you the question
- Thinking you must implement iterator()/indexOf()/equals() yourself — AbstractList already does, via get/size.
- Assuming an AbstractList subclass is mutable by default; the mutators throw UnsupportedOperationException until overridden.
- Forgetting that get must throw IndexOutOfBoundsException for invalid indices, which the inherited algorithms rely on.