Collections and Streams in Java
Absolute beginners who want a single, deep-dive document covering every common data structure and stream operation used in real Java code and DSA problems. Every section includes what it is, when to use it, how to create it, all key methods with examples, and gotchas to avoid.
Video Explanation

1. Collections Framework Overview
The Java Collections Framework is a unified architecture for storing and manipulating groups of objects. Every structure in this document lives under java.util.
Iterable
└── Collection
├── List → ordered, index-based, duplicates allowed
│ ├── ArrayList
│ └── LinkedList
├── Set → no duplicates
│ ├── HashSet
│ ├── LinkedHashSet
│ └── TreeSet
└── Queue → FIFO ordering
├── LinkedList
├── ArrayDeque
└── PriorityQueue
Map (NOT a Collection, but part of the framework)
├── HashMap
├── LinkedHashMap
└── TreeMap
Key interfaces to know:
List<E>— ordered sequence, access by indexSet<E>— unique elements onlyQueue<E>— FIFO; poll from front, add to rearDeque<E>— double-ended queue; use as stack or queueMap<K,V>— key-value pairs, keys are unique
2. ArrayList
What is it?
A resizable array backed by a plain Object[] under the hood. When the array fills up, Java creates a new array 1.5× the old size and copies everything over.
When to use it?
- You need fast random access by index (
O(1)). - You mostly add/read elements, not insert in the middle.
- Most common List you'll use in DSA (adjacency lists, storing results, etc.).
How to create
import java.util.ArrayList;
import java.util.List;
// Empty list
ArrayList<Integer> list = new ArrayList<>();
// With initial capacity (avoids resizing early — good for performance)
ArrayList<Integer> list2 = new ArrayList<>(100);
// From an existing collection
ArrayList<Integer> list3 = new ArrayList<>(List.of(1, 2, 3, 4, 5));
// Using the interface type (best practice)
List<String> names = new ArrayList<>();
Key Methods
Adding Elements
List<String> fruits = new ArrayList<>();
fruits.add("Apple"); // adds to end → ["Apple"]
fruits.add("Banana"); // → ["Apple", "Banana"]
fruits.add(0, "Mango"); // insert at index 0 → ["Mango", "Apple", "Banana"]
fruits.addAll(List.of("Kiwi", "Grape")); // add entire collection at end
fruits.addAll(1, List.of("Peach")); // add collection at index 1
Accessing Elements
String first = fruits.get(0); // "Mango" — O(1)
int size = fruits.size(); // total number of elements
// Iterate with for-each
for (String f : fruits) {
System.out.println(f);
}
// Iterate with index
for (int i = 0; i < fruits.size(); i++) {
System.out.println(i + ": " + fruits.get(i));
}
// forEach with lambda
fruits.forEach(f -> System.out.println(f));
Searching
boolean has = fruits.contains("Apple"); // true — O(n)
int idx = fruits.indexOf("Apple"); // first occurrence index, or -1
int lastIdx = fruits.lastIndexOf("Apple"); // last occurrence index, or -1
Modifying
fruits.set(0, "Papaya"); // replace element at index 0 — O(1)
Removing Elements
fruits.remove(0); // remove by index — O(n) due to shifting
fruits.remove("Banana"); // remove by value (first occurrence) — O(n)
fruits.removeAll(List.of("Kiwi", "Grape")); // remove all matching
fruits.retainAll(List.of("Apple", "Mango")); // keep ONLY these
fruits.clear(); // remove everything
Sorting
List<Integer> nums = new ArrayList<>(List.of(5, 2, 8, 1));
Collections.sort(nums); // natural order → [1, 2, 5, 8]
Collections.sort(nums, Collections.reverseOrder()); // reverse → [8, 5, 2, 1]
nums.sort(Comparator.naturalOrder()); // same as Collections.sort
nums.sort(Comparator.reverseOrder());
nums.sort((a, b) -> a - b); // custom lambda comparator
Sublist & Conversion
List<Integer> sub = nums.subList(1, 3); // [index 1, index 3) — view, not copy!
// List → Array
Integer[] arr = nums.toArray(new Integer[0]);
// Array → List (fixed size!)
List<Integer> fromArr = Arrays.asList(1, 2, 3); // cannot add/remove
// Mutable version:
List<Integer> mutable = new ArrayList<>(Arrays.asList(1, 2, 3));
Complexity
| Operation | Time |
|---|---|
get(i) | O(1) |
add(e) at end | O(1) amortized |
add(i, e) in middle | O(n) |
remove(i) | O(n) |
contains(e) | O(n) |
Common Gotchas
// Removing by int vs Integer!
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
list.remove(1); // removes by INDEX → list is [1, 3]
list.remove((Integer) 1); // removes by VALUE → list is [2, 3]
// ConcurrentModificationException — never modify while iterating
for (Integer n : list) {
list.remove(n); // CRASH! Use Iterator or removeIf instead
}
list.removeIf(n -> n % 2 == 0); // safe removal
3. LinkedList
What is it?
A doubly-linked list where each node holds a value plus pointers to the previous and next node. It implements both List and Deque.
When to use it?
- Frequent insertions/deletions at the beginning or middle —
O(1)once you have the position. - You need a queue or deque (it implements
Deque). - Avoid when you need random access —
get(i)isO(n).
How to create
import java.util.LinkedList;
LinkedList<Integer> ll = new LinkedList<>();
LinkedList<String> ll2 = new LinkedList<>(List.of("a", "b", "c"));
Key Methods
LinkedList<String> ll = new LinkedList<>();
// Adding
ll.add("B"); // adds to end
ll.addFirst("A"); // adds to front → ["A", "B"]
ll.addLast("C"); // adds to end → ["A", "B", "C"]
ll.add(1, "X"); // insert at index 1
// Accessing
String first = ll.getFirst(); // "A" — throws if empty
String last = ll.getLast(); // "C"
String elem = ll.get(1); // "X" — O(n)
// Peeking (returns null if empty, does NOT throw)
String peekF = ll.peekFirst(); // look at front without removing
String peekL = ll.peekLast(); // look at back without removing
// Removing
ll.removeFirst(); // removes & returns front
ll.removeLast(); // removes & returns back
ll.remove(1); // remove by index
ll.remove("B"); // remove by value
// Poll (returns null if empty, does NOT throw)
String polled = ll.pollFirst();
String polledL = ll.pollLast();
// Queue operations (uses the Queue interface)
ll.offer("Z"); // same as addLast
ll.poll(); // same as removeFirst
ll.peek(); // same as peekFirst
LinkedList as Stack
LinkedList<Integer> stack = new LinkedList<>();
stack.push(10); // addFirst
stack.push(20); // addFirst → [20, 10]
stack.pop(); // removeFirst → returns 20
stack.peek(); // peekFirst → returns 10
4. Stack
What is it?
A last-in, first-out (LIFO) data structure. In Java, Stack extends Vector (legacy). For new code, prefer ArrayDeque as a stack (it's faster and not synchronized).
When to use it?
- Implementing DFS (explicitly)
- Undo/redo features
- Balanced parentheses problems
- Expression evaluation
Using Stack (legacy class)
import java.util.Stack;
Stack<Integer> stack = new Stack<>();
stack.push(1); // [1]
stack.push(2); // [1, 2]
stack.push(3); // [1, 2, 3]
int top = stack.peek(); // 3 — look without removing
int popped = stack.pop(); // 3 — remove and return
boolean empty = stack.isEmpty(); // false
int size = stack.size(); // 2
boolean has = stack.contains(1); // true
int searchIdx = stack.search(2); // 1-based position from top
Using ArrayDeque as Stack (preferred)
import java.util.ArrayDeque;
import java.util.Deque;
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // addFirst
stack.push(2); // addFirst → top is 2
int top = stack.peek(); // 2
stack.pop(); // removes 2
5. Queue & Deque (ArrayDeque)
What is it?
Queue— first-in, first-out (FIFO). Add to rear, remove from front.Deque(double-ended queue) — can add/remove from both ends. Can be used as both a stack and a queue.ArrayDequeis the go-to implementation — backed by a resizable array, faster thanLinkedList.
When to use it?
- BFS (Breadth-First Search) — uses Queue
- Sliding window problems — uses Deque
- Monotonic queue — uses Deque
- Implementing LRU cache logic
Queue operations
import java.util.ArrayDeque;
import java.util.Queue;
Queue<String> queue = new ArrayDeque<>();
// Adding to rear
queue.add("A"); // throws NoSuchElementException if capacity exceeded
queue.offer("B"); // returns false if capacity exceeded (safer)
queue.offer("C");
// queue: [A, B, C] — front is A
// Peeking at front
String front = queue.peek(); // "A" — returns null if empty
String front2 = queue.element(); // "A" — throws if empty
// Removing from front
String removed = queue.poll(); // "A" — returns null if empty
String removed2 = queue.remove(); // "B" — throws if empty
queue.isEmpty(); // false
queue.size(); // 1
Deque operations
import java.util.ArrayDeque;
import java.util.Deque;
Deque<Integer> dq = new ArrayDeque<>();
// Adding
dq.addFirst(1); // [1]
dq.addLast(2); // [1, 2]
dq.offerFirst(0); // [0, 1, 2]
dq.offerLast(3); // [0, 1, 2, 3]
// Peeking
dq.peekFirst(); // 0 — null if empty
dq.peekLast(); // 3 — null if empty
// Removing
dq.pollFirst(); // 0
dq.pollLast(); // 3
dq.removeFirst(); // 1 — throws if empty
dq.removeLast(); // 2 — throws if empty
// Size check
dq.isEmpty();
dq.size();
Deque as Monotonic Queue (DSA Pattern)
// Sliding window maximum — classic DSA problem
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
int[] result = new int[nums.length - k + 1];
Deque<Integer> dq = new ArrayDeque<>(); // stores indices
for (int i = 0; i < nums.length; i++) {
// Remove indices outside window
while (!dq.isEmpty() && dq.peekFirst() < i - k + 1)
dq.pollFirst();
// Remove smaller elements from back (they're useless)
while (!dq.isEmpty() && nums[dq.peekLast()] < nums[i])
dq.pollLast();
dq.offerLast(i);
if (i >= k - 1)
result[i - k + 1] = nums[dq.peekFirst()];
}
6. PriorityQueue (Heap)
What is it?
A min-heap by default — poll() always returns the smallest element. Backed by a binary heap. Does not maintain insertion order.
When to use it?
- Dijkstra's algorithm
- Finding top-K elements
- Merge K sorted lists
- Any problem requiring repeated access to the min or max element
How to create
import java.util.PriorityQueue;
// Min-heap (default)
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// Max-heap
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// OR
PriorityQueue<Integer> maxHeap2 = new PriorityQueue<>((a, b) -> b - a);
// With initial capacity
PriorityQueue<Integer> pq = new PriorityQueue<>(100);
// Custom object heap (by second element of int[] pair)
PriorityQueue<int[]> pqCustom = new PriorityQueue<>((a, b) -> a[1] - b[1]);
Key Methods
PriorityQueue<Integer> pq = new PriorityQueue<>();
// Adding — O(log n)
pq.add(5);
pq.offer(1);
pq.add(3);
// Internal heap: [1, 5, 3] (heap order, not sorted!)
// Peek — O(1), does NOT remove
int min = pq.peek(); // 1 — returns null if empty
int min2 = pq.element(); // 1 — throws if empty
// Remove — O(log n)
int removed = pq.poll(); // 1 — returns null if empty
int removed2 = pq.remove(); // next min — throws if empty
// Size & check
pq.size();
pq.isEmpty();
pq.contains(3); // O(n) — heap doesn't support fast search
// Iterate (NOT in sorted order!)
for (int n : pq) {
System.out.println(n); // heap order, not guaranteed sorted
}
// To get sorted: keep polling
while (!pq.isEmpty()) {
System.out.println(pq.poll()); // this IS sorted
}
DSA Pattern — Top K Frequent Elements
// Keep a min-heap of size K; poll when size exceeds K
int[] nums = {1, 1, 1, 2, 2, 3};
int k = 2;
Map<Integer, Integer> freq = new HashMap<>();
for (int n : nums) freq.merge(n, 1, Integer::sum);
// Min-heap by frequency
PriorityQueue<Integer> pq = new PriorityQueue<>(
(a, b) -> freq.get(a) - freq.get(b)
);
for (int key : freq.keySet()) {
pq.offer(key);
if (pq.size() > k) pq.poll(); // remove least frequent
}
// pq now contains top K frequent elements
Complexity
| Operation | Time |
|---|---|
offer / add | O(log n) |
peek | O(1) |
poll | O(log n) |
contains | O(n) |
remove(Object) | O(n) |