JAVA / COLLECTIONS
The collections framework and choosing by behaviour
Choose a collection from the behaviour you need (duplicates, order, access pattern), declare the interface, and swap implementations freely.
What you will learn
- Translate a storage need into an interface: List, Set, Queue/Deque or Map
- Declare fields and parameters as the interface, name the class only after new
- Swap ArrayList for LinkedHashSet or TreeSet without touching calling code
- Spot optional operations: add on a fixed-size view compiles but throws
Understanding The collections framework and choosing by behaviour
The framework is two hierarchies, not one. Iterable sits at the top of the first: Collection extends it and then branches into List (positions, duplicates allowed), Set (no two elements equal by equals) and Queue/Deque (work at the ends). Map is separate because it stores key-value pairs rather than elements, which is why it has no add and cannot be handed to a for-each loop. Every one of those interfaces is a behavioural contract, and the concrete classes are competing ways of keeping the same promise.
Choosing by behaviour means answering three questions before you name a class: are duplicates meaningful information or a bug, does anything depend on iteration order, and do you reach elements by position, by key, by membership test, or only at the front and back? Those answers pick the interface, and only then do ordering and cost pick between its implementations. Reaching for ArrayList by reflex inverts the order, and you end up hand-writing duplicate checks and linear searches that a Set or a Map already performs for you.
Put the interface on the left of the assignment and the class on the right, as in Collection<String> tags = new LinkedHashSet<>(). Code that speaks only to the interface keeps compiling when you change that single line, which is what turns "which collection" into a decision you can revisit later instead of a commitment spread over every signature. The limit is that an interface type promises which methods exist, not that all of them work: add and remove are optional operations, and fixed-size or immutable views reject them at runtime.
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TreeSet;
public class ChooseByBehaviour {
// Speaks only Collection: add, size, contains, toString.
static Collection<String> fill(Collection<String> sink) {
for (String word : List.of("fig", "date", "fig", "apple", "date")) {
sink.add(word);
}
return sink;
}
static void report(Collection<String> c) {
System.out.println(c.getClass().getSimpleName()
+ " size=" + c.size()
+ " contains(date)=" + c.contains("date")
+ " " + c);
}
public static void main(String[] args) {
report(fill(new ArrayList<>())); // duplicates kept, arrival order
report(fill(new LinkedHashSet<>())); // duplicates dropped, first-seen order
report(fill(new TreeSet<>())); // duplicates dropped, sorted
report(fill(new ArrayDeque<>())); // duplicates kept, end access
}
}The interface you declare states the behaviour your code depends on, so pick it from the behaviour and leave the concrete class swappable.
Worked examples
An interface method that is not supported
Shows that a List variable guarantees the method exists, while the implementation decides whether it works.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class OptionalOps {
public static void main(String[] args) {
List<String> view = Arrays.asList("fig", "date", "apple");
view.set(0, "plum");
System.out.println(view);
try {
view.add("kiwi");
} catch (UnsupportedOperationException e) {
System.out.println("add on Arrays.asList: " + e.getClass().getSimpleName());
}
List<String> growable = new ArrayList<>(view);
growable.add("kiwi");
System.out.println(growable);
}
}Example explained
Line 1Arrays.asList returns a List backed by the array you passed, so set writes through and is supported.
Line 2add would have to change the length, which a fixed-length array cannot do, so the inherited default throws.
Line 3The compiler was satisfied because List declares add; only the implementation knows it is unsupported.
Line 4Copying with new ArrayList<>(view) produces an independent growable List where add succeeds.
Map joins the framework through views
Demonstrates that a Map is not a Collection and that its collection views are live windows onto the same data.
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class MapViews {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("fig", 4);
stock.put("date", 9);
Collection<Integer> counts = stock.values();
System.out.println("values size=" + counts.size());
stock.put("apple", 7);
System.out.println("values size after put=" + counts.size());
counts.remove(9);
System.out.println("date present? " + stock.containsKey("date"));
System.out.println("map size=" + stock.size());
}
}Example explained
Line 1Map declares put and get rather than add, and it is not Iterable, so it cannot be passed where a Collection is required.
Line 2values() hands back a Collection over the map's existing values, which is how a Map plugs into code written against Collection.
Line 3The second size call reports 3 without re-fetching the view, because the view is a window rather than a copy.
Line 4counts.remove(9) deletes one entry whose value equals 9, so a write through the view changes the map itself.
Important notes
Collection is deliberately weak: no get(int), no ordering promise. Widening a parameter to Collection is not free, so ask for List when the body needs positions.
Choosing a Set, or a Map key type, is also a promise about equals and hashCode on your element class; the interface cannot enforce that for you.
Common mistakes
Writing ArrayList<String> in fields, parameters and return types; when duplicates must later be rejected, every declaration and every caller has to change instead of one new expression.
Looping with for (String s : stock) over a Map, which does not compile because Map is not Iterable; the fix is to iterate keySet(), values() or entrySet(), not to switch to a List of pairs.
Treating List.of(...) or Arrays.asList(...) as an ordinary working List and calling add or remove later, which compiles cleanly and throws UnsupportedOperationException at runtime.
Try it yourself
Change, predict, then run
Take the tags ui, api, ui, db, api and produce three results with one shared loop: every tag in arrival order, unique tags in first-seen order, and unique tags alphabetically. Change only the object passed into the loop, never the loop body.
Open the Java workspaceCheck your understanding
A utility receives Collection<String> input and must report how many distinct values it holds without changing the caller's data. Which body respects the contracts it was given?
- return new HashSet<>(input).size();
- return (int) input.stream().count();
- Sort input in place, then count positions where the value differs from the previous one.
- Cast input to List, remove duplicates by index, then return input.size().
Show answer
Copying into a HashSet applies the Set contract, no two elements equal by equals, and leaves the argument untouched, so its size is the number of distinct values. Counting the stream is tempting but returns every element including repeats, which input.size() already tells you; the other two assume ordering or positional access that Collection never promises and they mutate data the caller still owns.