The Java collections framework is the set of container classes in java.util that hold objects for you: lists that keep order, sets that keep only unique elements, queues that hand elements back in the order they arrived, and maps that pair a key with a value. The point of the framework is not that these structures exist — you can build all of them from an array — but that they come with a shared vocabulary, so your code can work with any of them through the same few interfaces.
How is the framework organised?
Everything hangs off two root interfaces. Collection is the root for anything that holds elements one by one, and it splits three ways: List for ordered sequences that allow duplicates, Set for unordered collections with no duplicates, and Queue for collections that hand elements back in a defined order. Map is not a Collection at all — it stores pairs, so it sits on its own branch with no shared parent.
| Interface | Duplicates | Order | Looked up by | Typical implementations |
|---|---|---|---|---|
| List | allowed | insertion order, indexed | position | ArrayList, LinkedList |
| Set | rejected | unspecified | element value | HashSet, LinkedHashSet, TreeSet |
| Queue | allowed | arrival order | position from the head | ArrayDeque, PriorityQueue |
| Map | unique keys, values may repeat | unspecified | key | HashMap, LinkedHashMap, TreeMap |
Programming against the interface rather than the class is the habit worth forming. If a method takes a List<String>, you can pass an ArrayList today and a different implementation later without touching the method.
What can you do with any collection?
The Collection interface defines the operations that make sense for every container: add, remove, contains, size, isEmpty, and iteration. Writing one method that accepts the interface means it works for a list, a set, and a queue alike.
import java.util.Collection;
import java.util.List;
public class CommonOpsDemo {
static void report(String label, Collection<String> items) {
System.out.println(label);
System.out.println(" size: " + items.size());
System.out.println(" empty: " + items.isEmpty());
System.out.println(" contains \"b\": " + items.contains("b"));
if (!items.isEmpty()) {
System.out.println(" first element: " + items.iterator().next());
}
System.out.println(" all items: " + items);
}
public static void main(String[] args) {
report("a list", List.of("a", "b", "c"));
report("a set", List.of("a", "b", "b").stream().distinct().toList());
report("an empty list", List.of());
}
}
a list
size: 3
empty: false
contains "b": true
first element: a
all items: [a, b, c]
a set
size: 2
empty: false
contains "b": true
first element: a
all items: [a, b]
an empty list
size: 0
empty: true
contains "b": false
all items: []
One trap is visible in the code: calling iterator().next() on an empty collection throws NoSuchElementException, which is why the guard is there. A collection being empty is a completely normal state, and a method that receives one has to expect it.
How do the four families behave in practice?
Here they are side by side, doing the same kind of work. The list keeps everything in the order it was given. The set quietly drops the second copy of a repeated value. The queue hands back the element that arrived first. The map keeps one value per key, so putting the same key twice replaces the value without growing the map.
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
public class FourKindsDemo {
public static void main(String[] args) {
List<String> tasks = new ArrayList<>(List.of("write", "review", "ship"));
System.out.println("List keeps order and duplicates: " + tasks);
Set<String> tags = new LinkedHashSet<>(List.of("java", "java", "collections"));
System.out.println("Set drops duplicates: " + tags);
Queue<String> inbox = new ArrayDeque<>();
inbox.add("first");
inbox.add("second");
System.out.println("Queue serves head first: " + inbox.poll());
Map<String, Integer> ages = new LinkedHashMap<>();
ages.put("ada", 36);
ages.put("grace", 45);
ages.put("ada", 37);
System.out.println("Map replaces the value for a repeated key: " + ages);
System.out.println("Map lookup by key: " + ages.get("grace"));
System.out.println("Map size counts keys, not puts: " + ages.size());
Map<String, Integer> sorted = new TreeMap<>(ages);
System.out.println("A sorted map walks keys in order: " + sorted);
System.out.println("A hash set of the same words: " + new HashSet<>(List.of("b", "a", "b")));
}
}
List keeps order and duplicates: [write, review, ship]
Set drops duplicates: [java, collections]
Queue serves head first: first
Map replaces the value for a repeated key: {ada=37, grace=45}
Map lookup by key: 45
Map size counts keys, not puts: 2
A sorted map walks keys in order: {ada=37, grace=45}
A hash set of the same words: [a, b]
Two lines carry a warning. The map holds two entries even though three values were put in, because the third put replaced an existing key. And the set of words printed as [a, b] — the order of an unordered collection is not promised, so never write a test that depends on it.
Which factory method should you use to create a list?
There are three common ways and they are not interchangeable. List.of returns an immutable list, Arrays.asList returns a fixed-size view over an array, and new ArrayList<>(...) gives you a list you can grow and modify freely.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ImmutabilityDemo {
public static void main(String[] args) {
List<String> fixed = List.of("alpha", "beta");
List<String> backed = Arrays.asList("alpha", "beta");
List<String> growable = new ArrayList<>(List.of("alpha", "beta"));
System.out.println("all three start the same: " + fixed + " " + backed + " " + growable);
try {
fixed.add("gamma");
} catch (UnsupportedOperationException e) {
System.out.println("List.of refused the add: " + e.getClass().getSimpleName());
}
try {
backed.set(0, "changed");
System.out.println("Arrays.asList allowed the set: " + backed);
} catch (UnsupportedOperationException e) {
System.out.println("Arrays.asList refused the set: " + e.getClass().getSimpleName());
}
growable.add("gamma");
System.out.println("a new ArrayList accepted the add: " + growable);
}
}
all three start the same: [alpha, beta] [alpha, beta] [alpha, beta]
List.of refused the add: UnsupportedOperationException
Arrays.asList allowed the set: [changed, beta]
a new ArrayList accepted the add: [alpha, beta, gamma]
The middle case is the one that catches people: a list from Arrays.asList can have its elements replaced but cannot grow, because it is a window onto a fixed-size array. Using an immutable list where the code expects to modify it produces an UnsupportedOperationException at runtime, far from the line that created it, so pick the factory that matches the intent of the method that receives it.
How do you choose between them?
Work through the questions in order. Do you need to look things up by a key? Use a map. Do you need each item to appear at most once? Use a set. Do you need first-in-first-out handling? Use a queue. Do you need order, duplicates, and positions? Use a list. Do you need sorted output? Reach for TreeSet or TreeMap, or sort the list when you need it.
When two answers fit, pick the one that matches how the data will be read. A list that is scanned from front to back is often better as a set, because the set will not let duplicates in and membership tests stay cheap.
One last practical note: the collection interfaces are used throughout the standard library, so choosing the interface rather than the class pays off immediately. Methods such as Collections.sort, Collections.shuffle and Collections.addAll accept a List, and Collections.unmodifiableList hands back a list you cannot change. Code written against the interface keeps working when the implementation behind it changes, which is a cheap way to keep a design flexible without extra effort.
The framework on one page
Listfor ordered sequences with duplicates,Setfor unique elements,Queuefor arrival order,Mapfor key-value pairs.- Declare variables as the interface, so the implementation can change later.
- Anything a
Collectioncan do — add, remove, contains, size, iterate — is available on all three families. - Empty collections are normal, so check before pulling the first element out of one.
- Pick the factory method deliberately: immutable, fixed-size, or growable.
Try it yourself: take a list of twelve words with several duplicates and turn it into three collections: a list that keeps everything, a set that removes the duplicates, and a map from each word to the number of times it appears. Use Map.merge to count the repetitions. Print each result and check by hand that the counts match the number of times each word appeared in the original list.