Why Arrays Are Not Enough
An array is fast and simple, and it has one fixed limitation: its length is decided at creation and can never change. Storing a shopping cart in an array means guessing the maximum number of items up front, and then either wasting space or running out. Removing an item means shifting every later element down by one, by hand, and remembering to track how many slots are actually in use.
The Java Collections Framework is the standard library's answer. It is a set of interfaces and classes that give you resizable, well-tested data structures with the fiddly parts already written: adding, removing, searching, sorting, and checking for duplicates. It has been part of Java since version 1.2 and every Java program of any size uses it.
Its real strength is the design. The framework separates what a data structure does — the interface, such as List — from how it does it — the implementation, such as ArrayList. You write your code against the interface, and the implementation becomes a detail you can change later by editing one line.
That is why professional Java code almost always looks like List<String> names = new ArrayList<>(); rather than ArrayList<String> names = new ArrayList<>();. Declaring the variable by the interface means a method that takes a List works with any list, and switching to a LinkedList tomorrow requires one change instead of twenty. Get into this habit now — it costs nothing and interviewers notice.
import java.util.*;
// The array way: size fixed forever, removal done by hand
String[] cartArray = new String[10];
int used = 0;
cartArray[used++] = "Notebook";
// removing item 0 means shifting everything else down yourself
// The collections way
List<String> cart = new ArrayList<>(); // declared as List, created as ArrayList
cart.add("Notebook");
cart.add("Pen");
cart.add("Geometry box");
cart.remove("Pen"); // shifting handled for you
System.out.println(cart); // [Notebook, Geometry box]
System.out.println(cart.size()); // 2
System.out.println(cart.contains("Pen")); // false
// Because the variable is a List, this method takes any list
static void printAll(List<String> items) {
for (String item : items) {
System.out.println("- " + item);
}
}
printAll(cart);
printAll(new LinkedList<>(List.of("a", "b"))); // works unchanged import java.util.*;brings in the whole collections package in one line, which is convenient while learning. In real projects most editors import each class individually so a reader can see exactly what is being used.
The Four Families
Almost everything in the framework is one of four ideas, and choosing correctly is most of the skill. Ask two questions about your data: does the order matter, and are duplicates allowed?
A List is an ordered sequence that allows duplicates and gives each element a position, so you can ask for element number 3. This is the default choice and covers most needs — a cart, a list of students, lines read from a file.
A Set holds no duplicates. Adding an element that is already present simply does nothing, with no error. Use it whenever the question is membership rather than sequence: which subjects has this student opted for, which user IDs have already voted, which words appear in a document.
A Map stores key-to-value pairs and looks a value up by its key almost instantly. Reach for it whenever your sentence contains the word "per" or "by" — marks per subject, price by product code, count by word. A Map is not a Collection; it sits beside the others in the framework rather than under them.
A Queue or Deque controls the order things come out. A plain queue is first-in-first-out, like a ticket line. A Deque can add and remove at both ends, so it serves as a stack as well. PriorityQueue is the variant that always hands you the smallest element first, which matters in scheduling and in several standard algorithms.
List— ordered, duplicates allowed, indexed. Implementations:ArrayList,LinkedListSet— no duplicates. Implementations:HashSet(fastest, unordered),LinkedHashSet(keeps insertion order),TreeSet(kept sorted)Map— key to value. Implementations:HashMap,LinkedHashMap,TreeMapQueue/Deque— controlled order. Implementations:ArrayDeque,LinkedList,PriorityQueueList,SetandQueueall extendCollection, which extendsIterable— which is why the enhancedforloop works on all of themMapdeliberately does not extendCollection, because it stores pairs rather than single elements
import java.util.*;
// List — order matters, duplicates fine
List<String> attendance = new ArrayList<>();
attendance.add("Ananya");
attendance.add("Rahul");
attendance.add("Ananya"); // allowed
System.out.println(attendance); // [Ananya, Rahul, Ananya]
System.out.println(attendance.get(1)); // Rahul — by position
// Set — membership, no duplicates
Set<String> subjects = new HashSet<>();
subjects.add("Maths");
subjects.add("Physics");
System.out.println(subjects.add("Maths")); // false — already present
System.out.println(subjects.size()); // 2
// Map — look a value up by a key
Map<String, Integer> marks = new HashMap<>();
marks.put("Maths", 92);
marks.put("Physics", 87);
System.out.println(marks.get("Maths")); // 92
System.out.println(marks.getOrDefault("Hindi", 0)); // 0 — no NPE
// Deque used as a stack (preferred over the legacy Stack class)
Deque<String> history = new ArrayDeque<>();
history.push("page1");
history.push("page2");
System.out.println(history.pop()); // page2 — last in, first out
// Queue — first in, first out
Queue<String> tokens = new LinkedList<>();
tokens.offer("T-101");
tokens.offer("T-102");
System.out.println(tokens.poll()); // T-101 - You may still see
Vector,HashtableandStackin old code and old textbooks. They date from Java 1.0, synchronise every method whether you need it or not, and are slower as a result. UseArrayList,HashMapandArrayDequeinstead.
Generics: What the Angle Brackets Do
List<String> means "a list that holds Strings". Those angle brackets are generics, and they are what makes collections safe to use.
Before generics existed, a collection held plain Object, so anything could go in and everything came out as Object needing a cast. Put a number into a list of names by mistake and nothing complained until some unrelated line threw a ClassCastException at run time, far from the real error. With a type parameter, the compiler rejects the bad add on the spot and the values come out already the right type, with no cast at all.
Two syntax points. Generics only work with reference types, so a list of numbers is List<Integer>, never List<int>. Autoboxing makes that mostly invisible, but remember that list.get(i) returns an Integer that unboxes to int — and if the list can contain nulls, that unboxing can throw. The second point is the diamond <> on the right-hand side: new ArrayList<>() lets the compiler infer the type from the declaration so you do not write it twice.
Never write a raw type such as List names = new ArrayList();. It compiles, with a warning, and throws away every protection generics provide. It exists only so that code written before Java 5 still compiles.
import java.util.*;
// Raw type — legal, and a bad idea
List raw = new ArrayList();
raw.add("Ananya");
raw.add(42); // no complaint at all
// String s = (String) raw.get(1);
// ClassCastException at run time, far from the mistake
// Generic — the compiler is now on your side
List<String> names = new ArrayList<>();
names.add("Ananya");
// names.add(42);
// error: incompatible types: int cannot be converted to String
String name = names.get(0); // no cast needed
// Nested generics read inside-out: a Map from String to a List of Integers
Map<String, List<Integer>> marksBySubject = new HashMap<>();
marksBySubject.put("Maths", List.of(92, 88, 95));
marksBySubject.put("Physics", List.of(87, 79));
for (Map.Entry<String, List<Integer>> entry : marksBySubject.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// Wrapper types only — and beware unboxing a null
List<Integer> scores = new ArrayList<>();
scores.add(90); // autoboxed
scores.add(null); // allowed in an ArrayList!
int first = scores.get(0); // fine
// int second = scores.get(1); // NullPointerException on unboxing List.of(...),Set.of(...)andMap.of(...)(Java 9+) create compact immutable collections. They are ideal for fixed data, they rejectnulloutright, and callingadd()on one throwsUnsupportedOperationException. When you need a modifiable copy, wrap it:new ArrayList<>(List.of(...)).
Iterating, and ConcurrentModificationException
Every collection can be walked with the enhanced for loop, which is the right default. For a Map you choose what to walk: keySet() for the keys, values() for the values, or entrySet() for both together. Prefer entrySet() when you need both, because looping over keys and calling get() for each one does the lookup work twice.
Now the error that catches everyone. Removing an element from a collection while a for loop is iterating over it throws ConcurrentModificationException — and the name is misleading, because it happens in a single-threaded program with no concurrency anywhere. Java's collections are fail-fast: they keep a modification counter, and the iterator checks on each step that nothing changed behind its back. Structural modification during iteration means the iterator can no longer guarantee correct results, so it throws rather than silently skipping elements.
There are three correct fixes. The cleanest is removeIf(), added in Java 8, which takes a condition and removes everything matching it in one call. If you need more control, get an Iterator explicitly and call iterator.remove() — that is the one removal the iterator knows about and permits. The third option is to collect what you want to remove into a separate list during the loop and remove them afterwards.
One extra piece of nastiness: removing the second-to-last element sometimes appears to work without throwing, because of exactly how the check is implemented. That makes the bug intermittent, which is worse than a consistent failure. Never rely on it.
import java.util.*;
List<String> subjects = new ArrayList<>(
List.of("Maths", "Physics", "Chemistry", "Hindi"));
// ---- WRONG ----
// for (String s : subjects) {
// if (s.equals("Hindi")) subjects.remove(s);
// }
// ConcurrentModificationException
// ---- Fix 1: removeIf (cleanest) ----
subjects.removeIf(s -> s.equals("Hindi"));
System.out.println(subjects); // [Maths, Physics, Chemistry]
// ---- Fix 2: an explicit Iterator ----
Iterator<String> it = subjects.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.startsWith("C")) {
it.remove(); // the iterator's own remove is allowed
}
}
System.out.println(subjects); // [Maths, Physics]
// ---- Fix 3: collect first, remove after ----
List<String> toRemove = new ArrayList<>();
for (String s : subjects) {
if (s.length() > 5) toRemove.add(s);
}
subjects.removeAll(toRemove);
// ---- Iterating a Map ----
Map<String, Integer> marks = new LinkedHashMap<>();
marks.put("Maths", 92);
marks.put("Physics", 87);
for (Map.Entry<String, Integer> e : marks.entrySet()) { // both at once
System.out.println(e.getKey() + ": " + e.getValue());
}
for (String key : marks.keySet()) { /* keys only */ }
for (int value : marks.values()) { /* values only */ }
marks.forEach((k, v) -> System.out.println(k + " = " + v)); // Java 8+ - Adding to a collection during iteration throws the same exception for the same reason. If a loop needs to grow a collection, build a second list and combine them afterwards.
The Collections Helper Class, and Choosing Wisely
java.util.Collections — note the s, distinguishing it from the Collection interface — is a class of static helpers, in the same spirit as Arrays. Sorting, reversing, shuffling, finding the maximum and building unmodifiable wrappers all live there, and knowing it saves you writing loops that are easy to get subtly wrong.
Finally, the choice that actually matters in an interview and in real code: which implementation. The honest default is ArrayList for sequences and HashMap for lookups; those two cover the large majority of situations. Depart from them when you have a specific reason, and the reasons are short enough to memorise.
The rough performance picture is worth carrying in your head. ArrayList reads by index in constant time, and appending is effectively constant, but inserting or removing in the middle costs time proportional to the size because everything after it shifts. HashMap and HashSet look up in roughly constant time but keep no order. TreeMap and TreeSet keep everything sorted at a cost of logarithmic-time operations. LinkedHashMap and LinkedHashSet preserve insertion order for a small extra memory cost.
import java.util.*;
List<Integer> marks = new ArrayList<>(List.of(78, 92, 65, 88));
Collections.sort(marks); // [65, 78, 88, 92]
Collections.reverse(marks); // [92, 88, 78, 65]
Collections.shuffle(marks); // random order
System.out.println(Collections.max(marks)); // 92
System.out.println(Collections.min(marks)); // 65
List<String> votes = List.of("A", "B", "A", "C", "A");
System.out.println(Collections.frequency(votes, "A")); // 3
// Read-only view — attempts to modify throw
List<Integer> readOnly = Collections.unmodifiableList(marks);
// readOnly.add(100); // UnsupportedOperationException
// Sorting your own objects with a Comparator
record Student(String name, int marks) { }
List<Student> batch = new ArrayList<>(List.of(
new Student("Ananya", 87), new Student("Rahul", 92)));
batch.sort(Comparator.comparingInt(Student::marks).reversed());
System.out.println(batch.get(0).name()); // Rahul - Default to
ArrayList. ChooseLinkedListonly for heavy insertion and removal at the ends. - Default to
HashMap. ChooseLinkedHashMapwhen insertion order must be preserved,TreeMapwhen keys must stay sorted. - Use a
Setthe moment you catch yourself callinglist.contains()inside a loop — that combination is quietly quadratic. - Use
ArrayDequefor both stacks and queues; avoid the legacyStackclass. - Use
PriorityQueuewhen you always need the smallest or largest item next. - Use
List.of/Map.offor fixed data you never intend to modify.
- Anything you put into a
HashSet, or use as aHashMapkey, must have correctequals()andhashCode()methods. ForStringand the wrapper classes this is already true. For your own classes you must provide them, or use arecord, which generates them for you. The next two lessons cover exactly what goes wrong when you do not.
