Java Toolkit & Collections
Know which container to grab, what each call costs, and the handful of Java traps that sink interview code.
▶ guided lesson⚡ 1-minute summary↻ 14 recall cards
before this: Java OOP
01 · why it exists
Why this exists, and the weight it carries
since1998
The Collections Framework
Joshua Bloch (design lead) and the JDK team at Sun Microsystems
Early Java had arrays, Vector and Hashtable, with no shared interface between them. JDK 1.2 (renamed Java 2, shipped December 1998) added the Collections Framework: interfaces like List, Set and Map, plus ready-made implementations behind them. Joshua Bloch led its design. The old Vector and Hashtable were retrofitted to fit the new interfaces.
the problem it solvedEvery library invented its own containers, so code couldn't pass collections around, or swap a slow implementation for a faster one without a rewrite.
its jobs today
- Gives every Java API one vocabulary: a method that takes a
Collection<Order>accepts anArrayList, aHashSetor anArrayDeque. - Ships tested, tuned data structures in every JDK: resizable arrays, hash tables, ring buffers, heaps and red-black trees.
- Defines the types interviews speak: LeetCode's Java signatures use
List<Integer>,List<List<String>>and friends.
the weight it carries
From this chapter's own code: our stripped-down, int-only HashMap, ArrayList, ring-buffer deque and min-heap take 42 + 22 + 35 + 35 = 134 non-blank lines, with no generics and no iterators. With java.util each is one line: new HashMap<>(), new ArrayList<>(), new ArrayDeque<>(), new PriorityQueue<>(). In a timed round, those 134 lines are minutes you get back for the actual problem.
without it
Every solution would start with a hand-written hash table or heap, and every library would speak its own container dialect.
when · why · how
- when
- Always, unless the problem asks you to build the structure yourself (Design HashMap, Implement Queue using Stacks).
- why
- The JDK versions are tested, generic and fast, so your time goes into the algorithm. Knowing what's inside each one tells you the Big-O of every call.
- how
- Declare by interface, construct by class:
Map<String, Integer> m = new HashMap<>();·Deque<Integer> q = new ArrayDeque<>();
since1998
ArrayList (dynamic array)
Josh Bloch and Neal Gafter (ArrayList's credited authors) · Robert Tarjan (amortized analysis, 1985)
Java 1.0 already had Vector: an array that doubles when full (by default), with synchronized methods. JDK 1.2 (1998) added ArrayList, the same idea without the locking. Vector's own Javadoc now recommends ArrayList when you don't need thread safety. In JDK 17 it grows to old + (old >> 1), about 1.5×. Why is add still O(1)? Robert Tarjan's 1985 paper Amortized Computational Complexity explains the tool: average the cost over the whole sequence of operations.
the problem it solvedArrays have a fixed size, but you rarely know n in advance. Results, API responses and adjacency lists grow as you go.
its jobs today
- Holds most in-memory lists in Java code: query results, API responses, the
List<Integer>a LeetCode method returns. - Backs graph adjacency lists (
List<List<Integer>>) and rows whose length is only known at run time. - Gives O(1)
get(i): the slot's address is computed from i, never searched for.
the weight it carries
Illustrative: add 10 lakh (1,000,000) items to new ArrayList<>(). It grows only 30 times (10 → 15 → 22 → … → 1,215,487 slots) and copies 2,430,972 elements in total, about 2.4 copies per add. Growing by one slot each time (Arrays.copyOf(a, a.length + 1)) would copy 0 + 1 + … + 999,999 ≈ 5 × 10¹¹ elements.
without it
You'd either guess a size up front (and overflow or waste memory), or copy the whole array on every add: O(n²) for n adds.
when · why · how
- when
- You need an ordered list that you append to and read by index: results, paths, adjacency lists.
- why
get/setare O(1) andaddat the end is amortized O(1). Butadd(0, x)andremove(0)shift everything: O(n). For fixed-size numeric tables,int[]avoids boxing.- how
List<Integer> list = new ArrayList<>();When you know n,new ArrayList<>(n)skips every resize.
since1953
HashMap & HashSet (hash table)
Hans Peter Luhn (IBM) · Java's HashMap: Doug Lea, Josh Bloch, Arthur van Hoff, Neal Gafter
In January 1953 Hans Peter Luhn wrote an internal IBM memo: compute a number from a key's digits and put the record in that numbered bucket, so a search checks one bucket instead of the whole file. Knuth traces hashing back to this memo. Java's HashMap (JDK 1.2) follows the same plan: an array of buckets. In 2011 a public advisory showed that keys crafted to collide could slow servers down, Java included. Java 8 (2014, JEP 180) lets a crowded bucket turn into a balanced tree, so for keys that implement Comparable its worst case drops from O(n) to O(log n).
the problem it solvedFinding one record among millions by checking them one by one was too slow. Hashing jumps straight to the right bucket.
its jobs today
- Caches and lookups: user id → profile, product id → price, session token → session.
- Counting and grouping: word counts, orders per city, anagram groups.
- Duplicate checks with
HashSet: has this transaction id been processed already?
the weight it carries
Illustrative: 10 lakh (1,000,000) transaction ids in a HashMap. From 16 buckets it doubles 17 times to 2,097,152 buckets (1,000,000 > 0.75 × 1,048,576 = 786,432), so a bucket holds 1,000,000 / 2,097,152 ≈ 0.48 entries on average. get checks that one bucket; scanning a list checks up to 1,000,000. If every key collided, a pre-Java 8 chain walked up to 1,000,000 nodes; a Java 8+ tree bin of Comparable keys takes about 40 steps at most (height ≤ 2·log₂(n + 1) ≈ 39.9).
without it
Every "have I seen this?" becomes a full scan: O(n) per lookup and O(n²) for n lookups, like Two Sum with nested loops.
when · why · how
- when
- Membership, counting, grouping, or mapping a key to a value, when you don't need the keys in order.
- why
- O(1) average per
get/put/contains, paid for with O(n) memory and no ordering. Since Java 8 a crowded bucket ofComparablekeys costs O(log n), not O(n). - how
- Count:
map.merge(k, 1, Integer::sum). Group:map.computeIfAbsent(k, x -> new ArrayList<>()).add(v). Dedupe:set.add(x)returns false if x was already there.
since2006
ArrayDeque (ring buffer)
Josh Bloch and Doug Lea (Java 6)
Java 1.0's Stack extends Vector, so it inherits its synchronized methods. Java 1.2's LinkedList allocates a node object for every element. Java 6 (December 2006) added the Deque interface and ArrayDeque: one circular array with a moving head and tail. Its Javadoc says it is likely faster than Stack as a stack and faster than LinkedList as a queue. Stack's own Javadoc now points you to Deque.
the problem it solvedStacks and queues need O(1) at the ends, without locking overhead or one extra object per element.
its jobs today
- Queues for BFS: fewest moves on a game map, level-order tree traversal, web crawlers.
- Stacks for undo (Ctrl+Z), browser back, bracket matching, and DFS without recursion.
- Monotonic deques for sliding-window maximum.
the weight it carries
Illustrative: a print queue of 10 lakh (1,000,000) jobs. Draining it with ArrayList.remove(0) shifts 999,999 + 999,998 + … + 0 ≈ 5 × 10¹¹ elements. ArrayDeque.poll() just moves head: 1,000,000 O(1) steps and zero shifts.
without it
Queues built on ArrayList.remove(0) turn an O(n) BFS into O(n²), and Stack pays for locks you never needed.
when · why · how
- when
- Any stack (LIFO), any queue (FIFO), or when you need both ends at once.
- why
- Every end operation is O(1), amortized when the array has to grow. No shifting, no node per element. It rejects
null, sopoll()returning null always means empty. - how
- Stack:
Deque<Integer> st = new ArrayDeque<>(); st.push(x); st.pop();· Queue:Queue<Integer> q = new ArrayDeque<>(); q.offer(x); q.poll();
since1964
PriorityQueue (binary heap)
J. W. J. Williams · Java's PriorityQueue: Josh Bloch and Doug Lea (Java 5, 2004)
In 1964 J. W. J. Williams published Algorithm 232: Heapsort in Communications of the ACM, and with it the binary heap. It is a tree packed into an array: every parent is ordered before its children, so the top item sits at the front, and a slot's children are found by arithmetic on its index, so no pointers are needed. Java 5 (2004) added PriorityQueue, a binary min-heap in an array: the smallest item sits at index 0, and the children of index n live at 2n + 1 and 2n + 2.
the problem it solvedRepeatedly taking the smallest item while new items keep arriving, without re-sorting everything each time.
its jobs today
- Schedulers and timers: Java's
DelayQueuewraps aPriorityQueue, andScheduledThreadPoolExecutoruses a heap-based queue to pick the next task. - Dijkstra's shortest path in map routing: always expand the closest unsettled place next.
- Top-k: the 10 most searched terms, the k closest delivery partners, merging k sorted log files.
the weight it carries
Illustrative: 10 lakh (1,000,000) pending jobs. A heap that size has ⌊log₂ 1,000,000⌋ + 1 = 20 levels, so offer/poll move an item at most 19 times. Finding the minimum in an unsorted list costs 999,999 comparisons every time. Keeping a sorted ArrayList instead makes each insert shift up to 1,000,000 elements.
without it
"Give me the next smallest" would cost O(n) per call, so Dijkstra and every scheduler would crawl on big inputs.
when · why · how
- when
- You keep needing the smallest (or largest) item while items keep arriving: k largest, k closest, next event, merge k lists.
- why
offer/pollare O(log n) andpeekis O(1). The price: no full ordering, so iteration shows heap order andremove(x)/containsare O(n).- how
- Min-heap:
new PriorityQueue<>(). Max-heap:new PriorityQueue<>(Collections.reverseOrder()). Pairs:new PriorityQueue<int[]>((a, b) -> Integer.compare(a[0], b[0])).
since1972
TreeMap & TreeSet (red-black tree)
Rudolf Bayer (1972) · Leo Guibas and Robert Sedgewick (1978) · Java's TreeMap: Josh Bloch and Doug Lea
A plain binary search tree is fast only while it stays bushy. Insert keys in sorted order and it becomes a chain. In 1972 Rudolf Bayer described symmetric binary B-trees, which keep search, insert and delete at O(log n). In 1978 Leo Guibas and Robert Sedgewick recast the idea with red and black colours: the red-black tree. Java's TreeMap (JDK 1.2) is a red-black tree, and its Javadoc credits Cormen, Leiserson and Rivest's Introduction to Algorithms for the algorithms.
the problem it solvedKeeping keys sorted while inserting and deleting, with a guaranteed O(log n) per operation instead of a hopeful one.
its jobs today
- Leaderboards and price lists:
floorKey/ceilingKeyanswer "who is just above me?" or "best price ≤ ₹500" in O(log n). - Calendars and bookings: find the neighbouring bookings to check a new one for overlap.
- Time-series lookups:
floorEntry(t)gives the latest value at or before time t.
the weight it carries
Illustrative: 10 lakh (1,000,000) keys. A red-black tree's height is at most 2·log₂(n + 1) ≈ 39.9, so get, put and floorKey take about 40 comparisons at most. A plain BST fed sorted keys becomes a chain 1,000,000 nodes deep. A sorted ArrayList finds a key in 20 steps, but each insert shifts up to 1,000,000 elements.
without it
You couldn't keep keys sorted and insert fast at the same time: a sorted array pays O(n) per insert, and a plain BST can degrade to O(n) per search.
when · why · how
- when
- Keys must stay sorted, or you ask "nearest key ≤ x / ≥ x", "smallest or largest key", "all keys in a range".
- why
- Every operation is O(log n), guaranteed. Slower than HashMap's O(1) average, but HashMap can't answer order questions at all.
- how
TreeMap<Integer, String> m = new TreeMap<>();thenm.floorKey(x),m.ceilingKey(x),m.firstKey(),m.headMap(x). Sets:treeSet.lower(x),treeSet.higher(x).
since2004
Autoboxing & the Integer cache
The Java 5 language changes (JSR 201); Joshua Bloch led the JDK 5.0 language enhancements
Collections hold objects, so before Java 5 you wrapped every int by hand: list.add(new Integer(5)). Java 5 (2004) added autoboxing: the compiler now calls Integer.valueOf(5) for you. A fresh object for every small number would waste memory, so the Java Language Specification (§5.1.7) makes a pragmatic compromise, and Integer.valueOf always caches −128..127. Outside that range, identity is not promised.
the problem it solvedBoxing small numbers everywhere (counts, indices, flags) without creating a new object each time.
its jobs today
- Every
List<Integer>orMap<Character, Integer>of small counts reuses 256 shared objects instead of allocating new ones. - Keeps
map.merge(k, 1, Integer::sum)cheap while counts stay small: the boxed 1, 2, 3… come from the cache.
the weight it carries
Illustrative: a cricket app boxes 10 lakh (1,000,000) ball-by-ball run values (0–6) into a List<Integer>. Every one comes from the cache: 7 shared objects, zero new ones. Box 10 lakh order totals like ₹250 or ₹1,000 instead, and Integer.valueOf may create up to 1,000,000 separate objects. Then == between two equal totals can be false.
without it
Every boxed small number would cost a new object. And the flip side: if you don't know the cache exists, == on Integer passes your small tests and fails hidden tests with big values.
when · why · how
- when
- Whenever you compare
Integer,Longor other boxed values, especially values read back from a collection. - why
==on objects compares references. The cache makes it look right for −128..127;equalscompares values for every number.- how
a.equals(b),Objects.equals(a, b)(null-safe), or unbox first:int x = map.getOrDefault(k, 0);
02 · plain english
The idea in 30 seconds
Most interview solutions are 80% choosing the right container: ArrayList, ArrayDeque, HashMap/HashSet, TreeMap/TreeSet, PriorityQueue.
Each one is a data structure you'll meet later in the course. The JDK already wrote it for you. Know what's inside, and you know what each call costs.
A few traps (Integer ==, a - b comparators, remove(int) vs remove(Object)) cause most "it works on my example" bugs.
picture this
java.util is like the tools already installed on your laptop. Nobody writes a text editor before taking notes, but you do need to know when to open Notepad and when to open Excel. Same here: pick the container whose strengths match the job.
03 · remember
What you must know
- The map:
List(ArrayList) for indexed sequences ·Deque(ArrayDeque) for stacks and queues ·Set/Map(Hash, LinkedHash, Tree*) for membership and lookup ·PriorityQueuefor "give me the smallest next". - ArrayList is a growable array: when full it allocates about 1.5× the slots and copies everything. That's rare, so
addis amortized O(1). Butadd(0, x)andremove(0)shift every element: O(n). - Hash vs Linked vs Tree: HashMap is O(1) average with no order · LinkedHashMap is O(1) average and keeps insertion order · TreeMap is O(log n) with sorted keys plus floor/ceiling queries.
- Use `ArrayDeque`, not `Stack` or `LinkedList`, for stacks and queues.
Stackis a synchronized legacy class;ArrayDequeis a fast ring buffer. - int vs Integer: collections hold objects, so an
intis autoboxed.==on twoIntegers compares references. It only seems to work for −128..127 (the Integer cache). Useequals, or unbox. - Order objects with comparators:
Integer.compare(a, b)orComparator.comparingInt(...), nevera - b(overflow). Chain withthenComparing, flip withreversed(). - This chapter is the Collections Framework. Strings and
StringBuilderlive in Java arrays & strings, overflow in Java memory, fast I/O in Java I/O & debugging, generics and records in Java OOP. - Utilities you'll use daily:
Arrays.sort/fill/toString/asList/copyOf,Collections.sort/reverse/max,List.of,Map.getOrDefault/merge/computeIfAbsent.
04 · see it
Play with it in 3D
Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.
Inside ArrayList: growing by 1.5×
Adds are cheap writes. When the array is full, Java allocates about 1.5× the slots and copies everything. That's rare enough that add stays amortized O(1).
Inside HashMap: buckets, collisions, resizing
Each key is hashed to a bucket. Watch a collision form a chain, then the table double at load factor 0.75.
Inside ArrayDeque: a ring buffer
head moves instead of elements. Both ends are O(1): no shifting, unlike ArrayList.remove(0).
Inside PriorityQueue: a heap in an array
The same heap drawn as a tree and as the array Java actually stores.
The Integer cache trap
Why `a == b` is true for 127 and false for 128.
05 · cost
Time & space, explained
| Add | Remove | Lookup / contains | Order | why | |
|---|---|---|---|---|---|
| ArrayList | O(1) amortized at end, O(n) elsewhere | O(n) (O(1) at end) | get(i) O(1), contains O(n) | insertion | A resizable array: indexing is direct, but inserting/removing in the middle shifts elements. |
| LinkedList | O(1) at ends | O(1) at ends | get(i) O(n), contains O(n) | insertion | Doubly linked nodes: no shifting, but reaching index i walks the list. Rarely the best choice. |
| ArrayDeque | O(1) amortized at both ends | O(1) at both ends | contains O(n) | insertion | A circular array with a moving head: the go-to stack and queue. |
| HashSet / HashMap | O(1) average | O(1) average | O(1) average | none | Hash to a bucket; short chains on average. Big buckets become trees (Java 8+). |
| LinkedHashSet / LinkedHashMap | O(1) average | O(1) average | O(1) average | insertion (or access) | A HashMap plus a linked list through the entries to remember order. |
| TreeSet / TreeMap | O(log n) | O(log n) | O(log n), plus floor/ceiling | sorted | A red-black tree: always balanced, always sorted. |
| PriorityQueue | offer O(log n) | poll O(log n), remove(x) O(n) | peek O(1), contains O(n) | only the head is ordered | A binary heap in an array: the minimum is at index 0; the rest is only partially ordered. |
| Time | Notes | why | |
|---|---|---|---|
| Arrays.sort(int[]) | O(n log n) | dual-pivot quicksort; primitives, so stability doesn't matter | Sorting primitives in place; no boxing. |
| Arrays.sort(T[]) / Collections.sort / list.sort | O(n log n) | TimSort, stable; ~O(n) on nearly sorted input | Merge-based, so equal elements keep their order. Useful for multi-key sorts. |
| new PriorityQueue<>(collection) | O(n) | heapify, cheaper than n offers (O(n log n)) | Builds the heap bottom-up. |
| Arrays.fill / copyOf / equals / toString | O(n) | one pass | Touch every element once. |
| Arrays.binarySearch / Collections.binarySearch | O(log n) | input must already be sorted | Halves the range each step (on a RandomAccess list). |
| String += in a loop vs StringBuilder | O(n²) vs O(n) | += copies the whole string each time | Strings are immutable; StringBuilder appends are amortized O(1). |
06 · java
Raw vs Collections Framework
Arrays vs ArrayList
class RawArray { static int[] squares(int n) { int[] a = new int[n]; // fixed size, filled with 0s, primitives (no boxing) for (int i = 0; i < n; i++) a[i] = i * i; return a; } static int[] append(int[] a, int value) { int[] bigger = Arrays.copyOf(a, a.length + 1); // arrays can't grow: copy, O(n) bigger[a.length] = value; return bigger; } static int[][] grid(int rows, int cols, int fill) { int[][] g = new int[rows][cols]; for (int[] row : g) Arrays.fill(row, fill); // fill each row return g; }}class Lists { static List<Integer> squares(int n) { List<Integer> list = new ArrayList<>(); // grows on demand, holds Integer objects for (int i = 0; i < n; i++) list.add(i * i); // autoboxing int -> Integer return list; } static int[] toArray(List<Integer> list) { return list.stream().mapToInt(Integer::intValue).toArray(); // back to primitives } static List<List<Integer>> adjacency(int n) { List<List<Integer>> adj = new ArrayList<>(); // list of lists: the graph workhorse for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); return adj; }}int[]is fixed-size, stores primitives (fast, compact) and defaults to 0. Prefer it when the size is known: DP tables, counts, visited flags.ArrayList<Integer>grows on demand but stores references to boxedIntegerobjects. That is several times the memory of anint[], plus boxing cost.- Graphs:
List<List<Integer>>is the standard adjacency list. AvoidList<Integer>[]: generic arrays trigger unchecked warnings. list.size()vsarray.lengthvsstring.length(): three different spellings, a classic compile error under pressure.
Inside ArrayList: a growable array
class IntArrayList { private int[] data; // the backing array: capacity = data.length private int size = 0; // how many slots are actually used IntArrayList(int initialCapacity) { data = new int[initialCapacity]; } void add(int value) { if (size == data.length) grow(); // full? make room first data[size++] = value; // the cheap O(1) part } private void grow() { int oldCapacity = data.length; int newCapacity = oldCapacity + Math.max(1, oldCapacity >> 1); // about 1.5x, like java.util.ArrayList data = Arrays.copyOf(data, newCapacity); // copies every element: O(size) } int get(int i) { Objects.checkIndex(i, size); // same bounds check ArrayList.get does return data[i]; } int size() { return size; } int capacity() { return data.length; }}new ArrayList<>()starts with an empty array and allocates 10 slots on the firstadd. After that each grow isold + (old >> 1): 10 → 15 → 22 → 33 → … (OpenJDK 17).size()is how many you stored; the capacity stays hidden.get(i)checksi < size, not the capacity.- Know n up front?
new ArrayList<>(n)skips every resize. For primitives in hot loops,int[]is still faster (no boxing). remove(0)andadd(0, x)shift every element: O(n). That's why queues useArrayDeque.
Arrays & Collections utilities
class Utilities { static String demo() { int[] a = {5, 2, 9, 1}; Arrays.sort(a); // [1, 2, 5, 9] int pos = Arrays.binarySearch(a, 5); // 2 (array must be sorted) Integer[] boxed = {5, 2, 9, 1}; Arrays.sort(boxed, Collections.reverseOrder()); // descending needs objects List<Integer> fixed = Arrays.asList(3, 1, 2); // fixed-size view: set OK, add throws List<Integer> list = new ArrayList<>(fixed); // a real, growable copy Collections.sort(list); // [1, 2, 3] Collections.reverse(list); // [3, 2, 1] int max = Collections.max(list); // 3 int freq = Collections.frequency(List.of(1, 2, 1), 1); // 2 return Arrays.toString(a) + pos + Arrays.toString(boxed) + list + max + freq; }}Arrays.sort(int[])can't take a comparator. For descending order or custom keys, sort anInteger[], or sort ascending and read backwards.Arrays.asList(...)is a fixed-size view of the array:setworks,add/removethrow.List.of(...)is fully immutable and rejects nulls.- Need a mutable copy?
new ArrayList<>(List.of(...)).
HashMap: by hand vs java.util
class IntHashMap { private List<int[]>[] buckets; // each bucket: a chain of {key, value} private int size = 0; @SuppressWarnings("unchecked") IntHashMap(int capacity) { // capacity: a power of two buckets = new List[capacity]; } static int spread(int h) { return h ^ (h >>> 16); // same bit-mixing java.util.HashMap uses } private int indexFor(int key, int capacity) { return spread(Integer.hashCode(key)) & (capacity - 1); // fast "mod capacity" } void put(int key, int value) { int b = indexFor(key, buckets.length); if (buckets[b] == null) buckets[b] = new ArrayList<>(); for (int[] entry : buckets[b]) if (entry[0] == key) { entry[1] = value; return; } // key exists: overwrite buckets[b].add(new int[] {key, value}); // append to the chain if (++size > buckets.length * 3 / 4) resize(); // load factor 0.75 } Integer get(int key) { List<int[]> chain = buckets[indexFor(key, buckets.length)]; if (chain != null) for (int[] entry : chain) if (entry[0] == key) return entry[1]; return null; } @SuppressWarnings("unchecked") private void resize() { List<int[]>[] old = buckets; buckets = new List[old.length * 2]; // double, then re-bucket every entry: O(n) for (List<int[]> chain : old) if (chain != null) for (int[] e : chain) { int b = indexFor(e[0], buckets.length); if (buckets[b] == null) buckets[b] = new ArrayList<>(); buckets[b].add(e); } } int size() { return size; } int capacity() { return buckets.length; }}class Maps { static Map<String, Integer> wordCounts(String[] words) { Map<String, Integer> counts = new HashMap<>(); for (String w : words) counts.merge(w, 1, Integer::sum); // insert 1 or add 1 return counts; } static Map<Integer, List<String>> byLength(String[] words) { Map<Integer, List<String>> groups = new TreeMap<>(); // keys kept sorted for (String w : words) groups.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); return groups; } static String orders() { Map<String, Integer> linked = new LinkedHashMap<>(); // remembers insertion order linked.put("banana", 1); linked.put("apple", 2); linked.put("cherry", 3); TreeMap<Integer, String> tree = new TreeMap<>(Map.of(10, "ten", 20, "twenty", 30, "thirty")); return linked.keySet() + " " + tree.floorKey(25) + " " + tree.ceilingKey(25) + " " + tree.firstKey() + " " + tree.headMap(20).keySet() + " " + linked.getOrDefault("kiwi", 0); } static String sets() { Set<String> linked = new LinkedHashSet<>(List.of("b", "a", "c", "a")); // insertion order, no dups TreeSet<Integer> tree = new TreeSet<>(List.of(5, 1, 9, 3)); // sorted return linked + " " + tree + " " + tree.lower(5) + " " + tree.higher(5) + " " + tree.pollFirst(); }}- Real
HashMapdefaults: capacity 16, load factor 0.75, doubles on resize. The index is(n − 1) & (h ^ (h >>> 16)), exactly what the raw version does. - Java 8+: when a bucket already holding 8 entries gets another, it becomes a red-black tree (if the table has ≥ 64 buckets; otherwise the table is resized instead). Bad hash distributions then cost O(log n) per bucket for Comparable keys.
- Counting idioms:
map.merge(key, 1, Integer::sum)ormap.put(key, map.getOrDefault(key, 0) + 1). Grouping:map.computeIfAbsent(key, k -> new ArrayList<>()).add(x). - Iteration order: HashMap is unspecified (don't rely on it), LinkedHashMap keeps insertion order, TreeMap is sorted by key.
- Keys must have consistent
equalsandhashCode. Records and Strings do; arrays don't (int[]keys compare by identity, so useArrays.toString(arr)or aListas the key).
Stacks & queues: ring buffer vs ArrayDeque
class IntRing { private int[] data = new int[4]; private int head = 0; // index of the first element private int size = 0; void addLast(int x) { if (size == data.length) grow(); data[(head + size) % data.length] = x; // wrap around past the end size++; } void addFirst(int x) { if (size == data.length) grow(); head = (head - 1 + data.length) % data.length; // step back, wrapping to the end data[head] = x; size++; } int pollFirst() { if (size == 0) throw new NoSuchElementException(); int x = data[head]; head = (head + 1) % data.length; size--; return x; } int pollLast() { if (size == 0) throw new NoSuchElementException(); size--; return data[(head + size) % data.length]; } private void grow() { int[] bigger = new int[data.length * 2]; for (int i = 0; i < size; i++) bigger[i] = data[(head + i) % data.length]; // unwrap in order data = bigger; head = 0; } int size() { return size; }}class Library { static List<Integer> asStack(int[] values) { Deque<Integer> stack = new ArrayDeque<>(); // use instead of the legacy Stack class for (int v : values) stack.push(v); // push = addFirst List<Integer> out = new ArrayList<>(); while (!stack.isEmpty()) out.add(stack.pop()); // pop = removeFirst -> LIFO return out; } static List<Integer> asQueue(int[] values) { Queue<Integer> queue = new ArrayDeque<>(); // offer at the back, poll from the front for (int v : values) queue.offer(v); List<Integer> out = new ArrayList<>(); while (!queue.isEmpty()) out.add(queue.poll()); // FIFO return out; } static String peeks() { Deque<Integer> d = new ArrayDeque<>(List.of(1, 2, 3)); return d.peekFirst() + " " + d.peekLast() + " " + new ArrayDeque<Integer>().peekFirst(); // null when empty }}- Stack:
push/pop/peek(all at the front). Queue:offer/poll/peek(back in, front out). One class does both. ArrayDequerejectsnullelements;poll/peekreturnnullwhen empty, whilepop/remove/elementthrow.- Avoid
java.util.Stack(synchronized, extendsVector) and preferArrayDequeoverLinkedList(no node object per element).
Heaps: by hand vs PriorityQueue
class IntMinHeap { private int[] heap = new int[8]; private int size = 0; // children of i: 2i+1, 2i+2; parent: (i-1)/2 void offer(int x) { if (size == heap.length) heap = Arrays.copyOf(heap, size * 2); heap[size] = x; // add at the bottom int i = size++; while (i > 0 && heap[(i - 1) / 2] > heap[i]) { // smaller than parent? swap(i, (i - 1) / 2); i = (i - 1) / 2; } } int poll() { if (size == 0) throw new NoSuchElementException(); int min = heap[0]; heap[0] = heap[--size]; // last leaf goes to the root int i = 0; while (true) { int smallest = i, l = 2 * i + 1, r = 2 * i + 2; if (l < size && heap[l] < heap[smallest]) smallest = l; if (r < size && heap[r] < heap[smallest]) smallest = r; if (smallest == i) break; // heap order restored swap(i, smallest); i = smallest; } return min; } int peek() { return heap[0]; } int size() { return size; } private void swap(int a, int b) { int t = heap[a]; heap[a] = heap[b]; heap[b] = t; }}class Library { static List<Integer> sortedViaMinHeap(int[] values) { PriorityQueue<Integer> pq = new PriorityQueue<>(); // min-heap by default for (int v : values) pq.offer(v); List<Integer> out = new ArrayList<>(); while (!pq.isEmpty()) out.add(pq.poll()); return out; } static int kthLargest(int[] values, int k) { PriorityQueue<Integer> pq = new PriorityQueue<>(); // keep the k largest seen for (int v : values) { pq.offer(v); if (pq.size() > k) pq.poll(); // drop the smallest } return pq.peek(); } static List<String> byLengthDesc(List<String> words) { PriorityQueue<String> pq = new PriorityQueue<>( Comparator.comparingInt(String::length).reversed().thenComparing(Comparator.naturalOrder())); pq.addAll(words); List<String> out = new ArrayList<>(); while (!pq.isEmpty()) out.add(pq.poll()); return out; }}PriorityQueueis a min-heap. Max-heap:new PriorityQueue<>(Collections.reverseOrder())or a reversed comparator.- Printing or iterating a PriorityQueue shows heap order, not sorted order. Only repeated
poll()gives sorted output. remove(Object)andcontainsare O(n): a heap can only find its minimum quickly.
int vs Integer: autoboxing traps
class IntegerCache { /** Compares two boxed copies of the same int value, by reference and by value. */ static boolean[] compare(int value) { Integer a = value, b = value; // autoboxing calls Integer.valueOf(value) boolean sameObject = a == b; // compares REFERENCES boolean sameValue = a.equals(b); // compares values: always true here return new boolean[] {sameObject, sameValue}; }}class UnboxingTraps { static int countOrZero(Map<String, Integer> counts, String key) { // int c = counts.get(key); // NullPointerException if key is missing! return counts.getOrDefault(key, 0); // safe } static List<Integer> removeDemo() { List<Integer> list = new ArrayList<>(List.of(10, 20, 30, 1)); list.remove(1); // remove(int index): removes 20 list.remove(Integer.valueOf(1)); // remove(Object): removes the value 1 return list; // [10, 30] } static boolean sameCount(Map<Character, Integer> a, Map<Character, Integer> b, char c) { return Objects.equals(a.get(c), b.get(c)); // never == on Integer values }}Integer.valueOfcaches −128..127 (the upper bound can be raised with-XX:AutoBoxCacheMax). Outside it,==on a default JVM compares two different objects.- Unboxing
nullthrowsNullPointerException:int c = map.get(missingKey)crashes. UsegetOrDefault. list.remove(1)removes index 1 from aList<Integer>;list.remove(Integer.valueOf(1))removes the value 1.
Ordering: Comparable & Comparator
class Natural { /** A record with a natural order: by age, then by name. */ record Person(String name, int age) implements Comparable<Person> { @Override public int compareTo(Person other) { int byAge = Integer.compare(age, other.age); // never age - other.age return byAge != 0 ? byAge : name.compareTo(other.name); } } static List<Person> sorted(List<Person> people) { List<Person> copy = new ArrayList<>(people); Collections.sort(copy); // uses compareTo return copy; }}class Custom { static int[][] sortIntervals(int[][] intervals) { int[][] copy = intervals.clone(); Arrays.sort(copy, (a, b) -> Integer.compare(a[0], b[0])); // by start return copy; } static List<String> byLengthThenAlpha(List<String> words) { List<String> copy = new ArrayList<>(words); copy.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder())); return copy; } static Integer[] descending(int[] nums) { Integer[] boxed = Arrays.stream(nums).boxed().toArray(Integer[]::new); Arrays.sort(boxed, Collections.reverseOrder()); // int[] can't take a comparator return boxed; } static boolean subtractionBug() { Comparator<Integer> broken = (a, b) -> a - b; // overflows for far-apart values return broken.compare(Integer.MIN_VALUE, 1) > 0; // says MIN_VALUE > 1 ! }}Comparable= the one natural order of a type (compareTo).Comparator= any order you pass in (sorting, TreeMap, PriorityQueue).- Return negative / zero / positive.
Integer.compare(a, b)does it safely;a - boverflows for far-apart values. - Readable chains:
Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()).reversed().
| call | does | cost |
|---|---|---|
list.add(x) / list.get(i) / list.set(i, x) | ArrayList basics | O(1) amortized / O(1) / O(1) |
deque.push / pop / peek | Stack ops on ArrayDeque (front) | O(1) amortized |
deque.offer / poll / peek | Queue ops on ArrayDeque (back in, front out) | O(1) amortized |
map.getOrDefault(k, 0) | Value or a default (no null unboxing) | O(1) average |
map.merge(k, 1, Integer::sum) | Count occurrences in one call | O(1) average |
map.computeIfAbsent(k, x -> new ArrayList<>()) | Get-or-create a group | O(1) average |
treeMap.floorKey(k) / ceilingKey(k) | Greatest key ≤ k / smallest key ≥ k | O(log n) |
treeSet.first() / last() / pollFirst() | Min / max / remove min of a sorted set | O(log n) |
pq.offer(x) / pq.poll() / pq.peek() | Heap insert / remove min / see min | O(log n) / O(log n) / O(1) |
Arrays.sort(a) / Arrays.sort(objs, cmp) | Sort primitives / sort objects with a comparator | O(n log n) |
Collections.reverseOrder() | Comparator for descending order | O(1) |
new StringBuilder().append(x).reverse().toString() | Build, reverse, finish a string | O(n) total |
Character.isLetterOrDigit(c) / toLowerCase(c) | Classify / normalise characters | O(1) |
Math.floorMod(a, m) / Math.abs / Math.max | Non-negative modulo and friends | O(1) |
Integer.MAX_VALUE / Long.MAX_VALUE | Sentinels for min/max searches | O(1) |
Stack (LIFO) or queue (FIFO)
→ ArrayDeque
O(1) at both ends, faster than Stack and LinkedList.
Count things / group things
→ HashMap with merge / computeIfAbsent (int[26] for lowercase letters)
O(1) average per update; an array is even faster when keys are small integers.
"Have I seen it?"
→ HashSet
O(1) average add/contains.
Sorted keys, or nearest key ≤ / ≥ x
→ TreeMap / TreeSet
O(log n) with floor/ceiling/first/last.
Repeatedly take the smallest / largest
→ PriorityQueue (min-heap; reverse for max)
O(log n) offer/poll, O(1) peek.
Remember insertion order / build an LRU cache
→ LinkedHashMap (accessOrder = true + removeEldestEntry for LRU)
Hash speed with a predictable order.
A list you append to and index into
→ ArrayList (new ArrayList<>(n) if n is known)
O(1) get, amortized O(1) add at the end.
Fixed-size numbers (DP, counts, flags)
→ int[] / long[] / boolean[]
No boxing, contiguous memory, zero-initialised.
Build a string in a loop
→ StringBuilder
Amortized O(1) appends instead of O(n) copies.
07 · recognise
Spot the pattern
"Most recent", matching brackets, undo, monotonic stack
ArrayDeque as a stack
Level-by-level, BFS, "process in arrival order"
ArrayDeque as a queue
Frequencies, "how many times", grouping by a key
HashMap (merge / computeIfAbsent) or int[26]
"Closest value ≤ x", sorted keys while inserting, calendar/booking
TreeMap / TreeSet floor & ceiling
"k largest", "k closest", "next smallest event", merging k lists
PriorityQueue
Only lowercase letters
int[26] indexed by c − 'a'
Answers "modulo 10⁹+7" or values up to 10⁹ being multiplied
long arithmetic, reduce after each step
08 · traps & tricks
Mistakes to avoid, tricks to use
traps
- Comparing
IntegerorStringvalues with==: works for small numbers and literals, fails in hidden tests. int c = map.get(key)when the key may be absent → NullPointerException from unboxing.- Comparator
(a, b) -> a - bon values that can be far apart → overflow and a wrong order. - Expecting
PriorityQueueiteration ortoString()to be sorted. list.remove(i)on aList<Integer>removing an index when you meant the value.- Using
ArrayList.remove(0)as a queue: every poll shifts the whole list, O(n) each. Arrays.asList(arr).add(x)→ UnsupportedOperationException; andArrays.asList(intArray)gives aList<int[]>of one element.- Using
int[]as a HashMap key: arrays use identity equality, so convert to a String or List first. -7 % 3 == -1in Java. For indices that must be non-negative (circular arrays, hashing), useMath.floorMod.- Modifying a collection while looping over it with for-each → ConcurrentModificationException. Use an iterator's
removeorremoveIf.
tricks
- Declare by interface, construct by class:
Map<String, Integer> m = new HashMap<>();. Switching to TreeMap later is a one-word change. - Say why you picked a container in the interview: "ArrayDeque, because I need O(1) at both ends."
- Bas itna yaad rakho: stack or queue →
ArrayDeque, lookup →HashMap, order →TreeMap, next smallest →PriorityQueue. - Memorise the max-heap one-liner:
new PriorityQueue<>(Collections.reverseOrder()). - For
int[]pairs in a PriorityQueue:new PriorityQueue<int[]>((a, b) -> Integer.compare(a[0], b[0])). - Before submitting, scan for
intsums/products that could exceed 2·10⁹. - Keep
import java.util.*;at the top. LeetCode already includes it, but your local file and OAs may not.
09 · why & how
Questions you might be too shy to ask
Why can't I write List<int>?
Java generics only work with object types. int is a primitive, so collections use its wrapper Integer, and Java converts automatically (autoboxing). That's convenient but costs memory and time, which is why int[] is preferred for big numeric tables.
Why does a == b work for 100 but not for 1000 when both are Integer?
== checks whether two references point to the same object. Integer.valueOf keeps a cache of objects for −128..127, so both boxes of 100 are literally the same object. 1000 gets two separate objects. Use a.equals(b), or compare int values.
What do the empty <> in new HashMap<>() mean?
That's the diamond. The compiler copies the types from the left side for you. Map<String, Integer> m = new HashMap<>(); is the same as writing new HashMap<String, Integer>(). It saves typing and nothing else: no speed or behaviour difference.
HashMap or TreeMap?
HashMap by default: O(1) average. Pick TreeMap only when you need sorted keys or questions like "the largest key ≤ x" (floorKey). Those cost O(log n), but HashMap can't answer them at all.
How do I loop over a HashMap and get both the key and the value?
Loop over entrySet(): for (Map.Entry<String, Integer> e : map.entrySet()). Inside, e.getKey() and e.getValue() give you both, with no extra lookup. Need only keys? map.keySet(). Only values? map.values().
My HashMap of small Integer keys printed in sorted order. Can I rely on that?
No, it's a coincidence. An Integer's hash is the number itself, so in a fresh 16-bucket map keys 0..15 land in buckets 0..15, and printing walks the buckets in order. Now put 2 and 17 in a fresh map: it prints {17=…, 2=…}, because 17 % 16 = 1, so 17 sits in bucket 1, before 2's bucket 2. Need sorted? Use TreeMap. Need insertion order? Use LinkedHashMap.
What is Integer::sum inside map.merge(w, 1, Integer::sum)?
A method reference: a short way to write the lambda (oldValue, one) -> oldValue + one. If w is new, merge just stores 1. If w exists, it calls Integer.sum(old, 1) and stores the result. So {chai=2} becomes {chai=3}. Simple hai: one call for "insert or add".
If ArrayList sometimes copies the whole array, how is add O(1)?
Because the copies are rare and get rarer. In OpenJDK each grow makes room for about 50% more (an implementation detail: the Javadoc only promises "constant amortized time"). So 10 lakh adds trigger only about 30 grows and about 2.4 copies per add in total. Spread over all the adds, that's a constant: amortized O(1). A single add can still be O(n); the average never is.
Why ArrayDeque instead of Stack?
Stack is a legacy class built on Vector: its methods are synchronized (slower) and it exposes index-based methods a stack shouldn't have. The Java docs themselves recommend Deque implementations such as ArrayDeque.
ArrayDeque has push and offer, pop and poll. Which end does each one use?
push adds at the front; offer adds at the back. pop, poll and peek all work at the front. So push + pop = stack (LIFO), and offer + poll = queue (FIFO). Dhyan do: pick one pair per deque. Mixing push and offer on the same deque gives an order you didn't mean.
Is LinkedList ever the right choice?
Rarely in interviews. ArrayDeque beats it for stacks/queues, ArrayList beats it for indexing. It helps only when you hold an iterator and insert/remove in the middle in O(1). Even then, most problems want a hand-written linked list.
Do I need to know how HashMap works inside?
Be ready for it: "how does HashMap work?" is a common follow-up. This chapter's level is enough: an array of buckets, a hash picks the bucket, collisions share a bucket, the bucket array doubles once the map is more than 75% full (the load factor 0.75), and since Java 8 a crowded bucket becomes a tree. That lets you defend O(1) average and answer "what's the worst case?" calmly.
treeMap.floorKey(x) crashed my code with a NullPointerException. Why?
When no key is ≤ x, floorKey returns null (same for ceilingKey, lowerKey, higherKey). If you wrote int k = map.floorKey(x);, Java tries to unbox that null and throws. Store it as Integer k = map.floorKey(x); and check if (k != null) first.
There are so many methods. Do I have to memorise them all?
No. The ~15 calls in this chapter's API table cover almost every interview problem. Start with merge, getOrDefault, offer/poll, push/pop and floorKey; look the rest up while practising. They stick after a few problems.
10 · practice
Interview problems
Practise this chapter on these
Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.
- #242Valid AnagramEasyin Arrays & Strings
Pick the container for the alphabet:
int[26]for lowercase letters,HashMap<Character, Integer>for Unicode. - #49Group AnagramsMediumin Arrays & Strings
computeIfAbsentwith a String key, and why anint[]orchar[]can't be a HashMap key (identity equals/hashCode). - #347Top K Frequent ElementsMediumin Heaps
merge(x, 1, Integer::sum)counting, then aPriorityQueuewith a comparator: two Collections tools in one problem. - #20
- #729My Calendar IMediumin BST
TreeMap.floorEntry/ceilingEntryfind the two neighbouring bookings in O(log n).
11 · revise
The whole chapter on one screen
9 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.
- what it is
The Collections Framework = interfaces (
List,Deque,Set,Map) + ready-made data structures behind them. Pick by the operation you need most. - use it when
Stack/queue →
ArrayDeque. Seen it? Count? →HashSet/HashMap. Sorted or nearest key →TreeMap. Next smallest →PriorityQueue. Indexed list →ArrayList. - cost
HashMap O(1) avg · TreeMap O(log n) · PriorityQueue offer/poll O(log n), peek O(1) · ArrayList get O(1), add amortized O(1), remove(0) O(n).
- cost
ArrayList grows by 1.5× (
old + (old >> 1)): 10 lakh adds = only 30 grows (the first 10 slots, then 29 × 1.5), ~2.4 copies per add. Rare O(n) copies → amortized O(1). - template
map.merge(w, 1, Integer::sum);counts.map.computeIfAbsent(k, x -> new ArrayList<>()).add(v);groups. - template
Max-heap:
new PriorityQueue<>(Collections.reverseOrder()). Pairs:new PriorityQueue<int[]>((a, b) -> Integer.compare(a[0], b[0])). - trap
Integer==compares references: true for −128..127 (the cache), false for 128 on a default JVM. Useequals, or unbox toint. - trap
int c = map.get(k)on a missing key → NullPointerException.(a, b) -> a - boverflows. UsegetOrDefaultandInteger.compare. - pro tip
Declare by interface:
Deque<Integer> st = new ArrayDeque<>();. Then say why out loud: "ArrayDeque, O(1) at both ends."
12 · recall
Flip, remember, repeat
Why use ArrayDeque instead of Stack for a stack?
Question 1: Why use ArrayDeque instead of Stack for a stack?
13 · check yourself
Quick quiz
`Integer a = 128, b = 128;` On a default JVM, what is `a == b`?
Which class should implement a stack in modern Java?
A full ArrayList has capacity 10. You add one more element. What is the new capacity (JDK 17)?
Since Java 8, what is the worst-case cost of `get` when all of a HashMap's `String` keys land in one bucket?
You print a PriorityQueue after offering 3, 1, 2. What do you see?
Why is `(a, b) -> a - b` a risky comparator?
You need "the smallest key ≥ x" while also inserting keys. Which structure?
`List<Integer> list = new ArrayList<>(List.of(5, 6, 7)); list.remove(1);` What's left?
Which call counts word frequencies in one line?
14 · learn more
A second teacher, if you want one
- ▶ playlistJava Collections Framework Complete Course 2026 | From Zero to Professional ↗Engineering Digest · HindiThe whole Collections Framework in Hindi, one class per video: ArrayList, HashMap, TreeMap, sets, queues, Comparable and Comparator.
- § docsTrail: Collections ↗Oracle · The Java Tutorials · EnglishOracle's own tour of the collection interfaces, implementations and algorithms. Come back to it whenever you forget which class to use.
- ✎ articleThe Java HashMap Under the Hood ↗Baeldung · EnglishWhat really happens on put() and get(): buckets, collisions, load factor, rehashing, and the Java 8 switch to trees. Interviewers like to ask about these internals.