An interface describes behavior without requiring callers to depend on one implementation. Generics describe the types that behavior works with. Collections combine both ideas: you can ask for a List<String> while choosing an ArrayList implementation. This Java 21 lesson connects the ideas through a small reusable store.
Separate a contract from its implementation
An interface may declare abstract methods, default methods with implementations, and static helpers. A class can implement several interfaces even though it can extend only one class. Implementations of public interface methods must be public. Interface fields are constants, not per-object mutable state.
Save as CollectionsDemo.java, then compile with javac --release 21 CollectionsDemo.java and run java CollectionsDemo.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CollectionsDemo {
interface Store<T> {
void add(T item);
List<T> snapshot();
}
static final class MemoryStore<T> implements Store<T> {
private final List<T> items = new ArrayList<>();
@Override
public void add(T item) {
items.add(java.util.Objects.requireNonNull(item));
}
@Override
public List<T> snapshot() {
return List.copyOf(items);
}
}
static double total(List<? extends Number> values) {
double result = 0;
for (Number value : values) {
result += value.doubleValue();
}
return result;
}
public static void main(String[] args) {
Store<String> store = new MemoryStore<>();
store.add("Java");
store.add("Go");
store.add("Java");
List<String> saved = store.snapshot();
store.add("Rust");
Set<String> unique = new LinkedHashSet<>(saved);
Map<String, Integer> counts = new LinkedHashMap<>();
for (String name : saved) {
counts.merge(name, 1, Integer::sum);
}
List<String> sorted = new ArrayList<>(unique);
sorted.sort(Comparator.naturalOrder());
System.out.println(saved);
System.out.println(sorted);
System.out.println(counts);
System.out.println(total(List.of(2, 3, 4)));
}
}
Expected output:
[Java, Go, Java]
[Go, Java]
{Java=2, Go=1}
9.0
Store<String> prevents storing an integer accidentally. The diamond <> lets the compiler infer type arguments. List.copyOf gives callers an unmodifiable snapshot whose membership does not change when the store grows. Its elements are not deep-copied. It also rejects null elements, which is why the store rejects them at insertion.
Choose a collection by its promise
| Need | Interface and usual starting implementation |
|---|---|
| Ordered values, duplicates allowed, indexed access | List, usually ArrayList |
| Unique values | Set, usually HashSet |
| Lookup by a unique key | Map, usually HashMap |
| Queue or stack operations at the ends | Deque, usually ArrayDeque |
Hash-based sets and maps do not promise iteration order. The linked implementations in the example preserve insertion order so the output is predictable. TreeMap and TreeSet maintain sorted order. Array-list indexing is constant time, while insertion near the front shifts later elements. Hash lookup is typically constant time with suitable keys, not an unconditional worst-case guarantee.
A map is not a subtype of Collection. merge combines a new value with an existing value for a key; Integer::sum is a method reference, covered next with lambdas. Custom hash keys need stable, consistent equals and hashCode. Sorted collections use their comparator to determine equivalence, so choose an ordering consistent with your intended uniqueness rule.
Read generic bounds
T is a type parameter, not a runtime placeholder you can fill with any unrelated value. Generics use reference types: write List<Integer>, not a list of primitive int. Avoid raw types such as bare List, which discard useful checking and can defer failures to runtime casts.
Generics are invariant: List<Integer> is not a subtype of List<Number>. Otherwise a caller could insert a Double into a list promised to contain integers. List<? extends Number> accepts lists of numbers or their subtypes for reading as Number, but does not let you add arbitrary numbers. List<? super Integer> lets you add integers, while reads have only the guaranteed type Object.
A generic method declares its parameter before its return type, as in static <T> T first(List<T> items). A bound such as T extends Comparable<T> requires a supported operation. Most generic type information is erased during compilation; you cannot test instanceof List<String> or directly create new T().
Pitfalls, exercise and solution guidance
List.of is unmodifiable; Arrays.asList is a fixed-size view backed by an array. Neither behaves like a resizable ArrayList. Do not structurally modify an ordinary list inside an enhanced loop; use removeIf, an iterator's removal operation, or collect changes separately.
Add contains(T item) to the store contract and implementation. Test duplicates, a missing value, and a second Store<Integer>. Delegate to items.contains, which uses equality. Then try to add to a snapshot and confirm UnsupportedOperationException; the original store should remain usable. Explain why returning items directly would break encapsulation.
Official references
- Java interfaces.
- Collections framework tutorials.
- Java 21 List API.
- Oracle tutorial: wildcard guidelines.
Previous: Classes and Objects. Next: Exceptions, Files and Resources.

