Java's Collections Framework, added in Java 2 and massively enhanced in Java 8, is the toolkit every Java developer uses daily. Combined with Generics (type-safe containers) and the Stream API (functional-style data processing), these features let you write concise, powerful, and readable code. This guide covers every major concept with working code.
Step 1 — Java Collections Framework Overview
The Collections Framework is a unified architecture for storing and manipulating groups of objects. All collections are in the java.util package.
Core Interfaces and Their Implementations
- List (ordered, allows duplicates): ArrayList, LinkedList, Vector, Stack
- Set (no duplicates): HashSet, LinkedHashSet, TreeSet
- Map (key-value pairs): HashMap, LinkedHashMap, TreeMap, Hashtable
- Queue (FIFO ordering): LinkedList, ArrayDeque, PriorityQueue
- Deque (double-ended queue): ArrayDeque, LinkedList
Step 2 — List: ArrayList vs LinkedList
import java.util.*;
public class ListExamples {
public static void main(String[] args) {
// ============================================
// ARRAYLIST: backed by a dynamic array
// Best for: random access by index, frequent reads
// Worst for: frequent insertions/deletions in the middle
// ============================================
List<String> fruits = new ArrayList<>(); // always program to interface!
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
fruits.add(0, "Avocado"); // insert at index 0 (O(n) — shifts elements)
System.out.println(fruits); // [Avocado, Apple, Banana, Cherry]
System.out.println(fruits.get(2)); // Banana (O(1) random access)
System.out.println(fruits.size()); // 4
System.out.println(fruits.contains("Apple")); // true (O(n) linear search)
System.out.println(fruits.indexOf("Banana")); // 2
fruits.remove("Apple"); // remove by value (O(n))
fruits.remove(0); // remove by index (O(n))
System.out.println(fruits); // [Banana, Cherry]
// Sorting
List<Integer> nums = new ArrayList<>(Arrays.asList(5, 2, 8, 1, 9, 3));
Collections.sort(nums); // natural order: [1, 2, 3, 5, 8, 9]
nums.sort(Comparator.reverseOrder()); // [9, 8, 5, 3, 2, 1]
System.out.println(nums);
// Creating a fixed-size list from an array (IMMUTABLE!)
List<String> immutable = List.of("one", "two", "three"); // Java 9+
// immutable.add("four"); // throws UnsupportedOperationException
// ============================================
// LINKEDLIST: backed by a doubly linked list
// Best for: frequent insertions/deletions at head or tail
// Worst for: random access by index (O(n))
// ============================================
LinkedList<String> queue = new LinkedList<>();
queue.addFirst("first"); // O(1) — add to head
queue.addLast("second"); // O(1) — add to tail
queue.addFirst("zero"); // O(1) — add to head
System.out.println(queue.getFirst()); // zero
System.out.println(queue.getLast()); // second
queue.removeFirst(); // O(1)
System.out.println(queue); // [first, second]
}
}Step 3 — Set: HashSet, LinkedHashSet, TreeSet
import java.util.*;
public class SetExamples {
public static void main(String[] args) {
// ============================================
// HASHSET: no duplicates, NO guaranteed order
// Backed by HashMap. O(1) add/remove/contains.
// ============================================
Set<String> hashSet = new HashSet<>();
hashSet.add("Banana");
hashSet.add("Apple");
hashSet.add("Cherry");
hashSet.add("Apple"); // duplicate — silently ignored
System.out.println(hashSet.size()); // 3
System.out.println(hashSet.contains("Banana")); // true (O(1))
System.out.println(hashSet); // [Banana, Cherry, Apple] — ORDER NOT GUARANTEED
// ============================================
// LINKEDHASHSET: no duplicates, INSERTION ORDER maintained
// ============================================
Set<String> linkedSet = new LinkedHashSet<>();
linkedSet.add("Banana");
linkedSet.add("Apple");
linkedSet.add("Cherry");
linkedSet.add("Apple"); // ignored
System.out.println(linkedSet); // [Banana, Apple, Cherry] — insertion order!
// ============================================
// TREESET: no duplicates, SORTED order (natural or Comparator)
// O(log n) add/remove/contains — backed by Red-Black Tree
// ============================================
TreeSet<Integer> treeSet = new TreeSet<>();
treeSet.add(5); treeSet.add(2); treeSet.add(8); treeSet.add(1);
System.out.println(treeSet); // [1, 2, 5, 8] — always sorted
System.out.println(treeSet.first()); // 1 (smallest)
System.out.println(treeSet.last()); // 8 (largest)
System.out.println(treeSet.headSet(5)); // [1, 2] — elements < 5
System.out.println(treeSet.tailSet(5)); // [5, 8] — elements >= 5
// Set operations: union, intersection, difference
Set<Integer> a = new HashSet<>(Arrays.asList(1, 2, 3, 4));
Set<Integer> b = new HashSet<>(Arrays.asList(3, 4, 5, 6));
Set<Integer> union = new HashSet<>(a);
union.addAll(b); // {1,2,3,4,5,6}
Set<Integer> intersection = new HashSet<>(a);
intersection.retainAll(b); // {3,4}
Set<Integer> difference = new HashSet<>(a);
difference.removeAll(b); // {1,2}
System.out.println("Union: " + union);
System.out.println("Intersection: " + intersection);
System.out.println("Difference: " + difference);
}
}Step 4 — Map: HashMap, LinkedHashMap, TreeMap
import java.util.*;
public class MapExamples {
public static void main(String[] args) {
// ============================================
// HASHMAP: key-value pairs, NO order, O(1) average
// Keys must properly implement equals() and hashCode()
// ============================================
Map<String, Integer> wordCount = new HashMap<>();
wordCount.put("java", 10);
wordCount.put("python", 7);
wordCount.put("go", 5);
wordCount.put("java", 15); // overwrites — keys are unique!
System.out.println(wordCount.get("java")); // 15
System.out.println(wordCount.get("rust")); // null (key not found)
System.out.println(wordCount.getOrDefault("rust", 0)); // 0 — safe!
System.out.println(wordCount.containsKey("python")); // true
System.out.println(wordCount.size()); // 3
// putIfAbsent: only puts if key doesn't exist
wordCount.putIfAbsent("java", 99); // ignored: 'java' already exists
wordCount.putIfAbsent("rust", 3); // added: 'rust' was absent
// merge: powerful for counting/accumulating
String text = "apple banana apple cherry apple banana";
Map<String, Integer> freq = new HashMap<>();
for (String word : text.split(" ")) {
freq.merge(word, 1, Integer::sum); // key, initial value, merge function
}
System.out.println(freq); // {banana=2, cherry=1, apple=3}
// compute: update value based on existing value
freq.compute("apple", (k, v) -> v == null ? 1 : v + 10);
System.out.println(freq.get("apple")); // 13
// Iterating a Map — three ways
// 1. entrySet() — most efficient, gives key AND value
for (Map.Entry<String, Integer> entry : wordCount.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 2. keySet() — then get value
for (String key : wordCount.keySet()) {
System.out.println(key + " -> " + wordCount.get(key));
}
// 3. forEach (lambda — Java 8+)
wordCount.forEach((k, v) -> System.out.println(k + ": " + v));
// ============================================
// LINKEDHASHMAP: insertion order maintained
// ============================================
Map<String, String> capitals = new LinkedHashMap<>();
capitals.put("India", "New Delhi");
capitals.put("USA", "Washington D.C.");
capitals.put("Japan", "Tokyo");
System.out.println(capitals); // {India=New Delhi, USA=Washington D.C., Japan=Tokyo}
// ============================================
// TREEMAP: sorted by key (natural order or Comparator)
// ============================================
TreeMap<String, Integer> sorted = new TreeMap<>(wordCount);
System.out.println(sorted); // alphabetically sorted
System.out.println(sorted.firstKey()); // smallest key
System.out.println(sorted.lastKey()); // largest key
System.out.println(sorted.headMap("p")); // keys strictly less than "p"
}
}Step 5 — Queue and Deque
import java.util.*;
public class QueueDeque {
public static void main(String[] args) {
// ============================================
// QUEUE (FIFO — First In, First Out)
// Use ArrayDeque for a simple queue (faster than LinkedList)
// ============================================
Queue<String> queue = new ArrayDeque<>();
queue.offer("Task-1"); // enqueue (add to tail)
queue.offer("Task-2");
queue.offer("Task-3");
System.out.println(queue.peek()); // "Task-1" — look at head WITHOUT removing
System.out.println(queue.poll()); // "Task-1" — remove and return head
System.out.println(queue.size()); // 2
// Note: add() throws exception on failure; offer() returns false.
// remove() throws exception if empty; poll() returns null.
// element() throws exception; peek() returns null.
// ============================================
// PRIORITYQUEUE: retrieves elements by priority (min-heap by default)
// ============================================
PriorityQueue<Integer> pq = new PriorityQueue<>(); // min-heap
pq.offer(5); pq.offer(1); pq.offer(3); pq.offer(2);
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " "); // 1 2 3 5 — always dequeues the SMALLEST
}
System.out.println();
// Max-heap: use reverse comparator
PriorityQueue<Integer> maxPQ = new PriorityQueue<>(Comparator.reverseOrder());
maxPQ.offer(5); maxPQ.offer(1); maxPQ.offer(3);
System.out.print(maxPQ.poll()); // 5 — largest first
// ============================================
// DEQUE (double-ended queue) — use as Stack or Queue
// ============================================
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("B"); // add to front
deque.addLast("C"); // add to back
deque.addFirst("A"); // add to front
System.out.println(deque); // [A, B, C]
System.out.println(deque.pollFirst()); // A
System.out.println(deque.pollLast()); // C
// Using Deque as a Stack (LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("first"); // addFirst
stack.push("second"); // addFirst
stack.push("third"); // addFirst
System.out.println(stack.pop()); // "third" — LIFO
System.out.println(stack.pop()); // "second"
}
}Step 6 — Generics: Type-Safe Code
Generics allow you to write code that works with any type while maintaining type safety at compile time. They eliminate the need for casting and prevent ClassCastException at runtime.
import java.util.*;
// Generic class: T is a type parameter (placeholder)
public class Pair<T, U> {
private T first;
private U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() { return first; }
public U getSecond() { return second; }
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
}
// Generic method: type parameter declared before return type
class Utils {
// Swap two elements in a list
public static <T> void swap(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
// Return the maximum of two Comparable objects
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
// <T extends Comparable<T>> means T must implement Comparable
}
// Bounded wildcard: ? extends Number — reads a list of any Number subtype
public static double sumList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) sum += n.doubleValue();
return sum;
// Why not <T extends Number>? Because List<Integer> is NOT a List<Number>
// ? extends Number (upper-bounded wildcard) handles this correctly
}
// Lower-bounded wildcard: ? super Integer — writes Integers into a list
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) list.add(i);
}
}
class GenericsDemo {
public static void main(String[] args) {
// Using generic class with different types
Pair<String, Integer> p1 = new Pair<>("age", 25);
Pair<Double, Boolean> p2 = new Pair<>(3.14, true);
System.out.println(p1); // (age, 25)
System.out.println(p2); // (3.14, true)
// String getFirst() — type safe, no casting needed!
String name = p1.getFirst(); // type is String at compile time
// Generic method
List<String> words = new ArrayList<>(Arrays.asList("a", "z", "m"));
Utils.swap(words, 0, 2);
System.out.println(words); // [m, z, a]
System.out.println(Utils.max(10, 20)); // 20
System.out.println(Utils.max("apple", "banana")); // banana
// Upper-bounded wildcard
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
System.out.println(Utils.sumList(ints)); // 6.0
System.out.println(Utils.sumList(doubles)); // 4.0
}
}Step 7 — Lambda Expressions and Functional Interfaces
Lambda expressions (Java 8) let you write anonymous functions concisely. A lambda can be used anywhere a functional interface (an interface with exactly one abstract method) is expected.
import java.util.*;
import java.util.function.*;
public class Lambdas {
public static void main(String[] args) {
// --- Lambda syntax ---
// (parameters) -> expression // single expression, implicit return
// (parameters) -> { statements; } // block body, explicit return
// === Built-in Functional Interfaces (java.util.function) ===
// Runnable: () -> void
Runnable r = () -> System.out.println("Running!");
r.run();
// Supplier<T>: () -> T — produces a value, takes no input
Supplier<String> greeting = () -> "Hello, World!";
System.out.println(greeting.get()); // Hello, World!
Supplier<List<String>> listFactory = ArrayList::new; // constructor reference
List<String> list = listFactory.get();
// Consumer<T>: T -> void — consumes a value, produces nothing
Consumer<String> printer = s -> System.out.println(">> " + s);
Consumer<String> upper = s -> System.out.println(s.toUpperCase());
Consumer<String> combined = printer.andThen(upper); // chains consumers
combined.accept("hello"); // >> hello \n HELLO
// BiConsumer<T, U>: (T, U) -> void
BiConsumer<String, Integer> printPair = (k, v) ->
System.out.println(k + " = " + v);
printPair.accept("count", 42);
// Function<T, R>: T -> R — transforms input to output
Function<String, Integer> strLen = s -> s.length();
Function<Integer, Boolean> isEven = n -> n % 2 == 0;
// compose: isEven(strLen(input))
Function<String, Boolean> isEvenLength = strLen.andThen(isEven);
System.out.println(isEvenLength.apply("Hello")); // false (length 5)
System.out.println(isEvenLength.apply("Java")); // true (length 4)
// BiFunction<T, U, R>: (T, U) -> R
BiFunction<String, String, String> concat = (a, b) -> a + " " + b;
System.out.println(concat.apply("Hello", "Java")); // Hello Java
// Predicate<T>: T -> boolean — test a condition
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startsWithJ = s -> s.startsWith("J");
Predicate<String> combined2 = isLong.and(startsWithJ); // AND
Predicate<String> either = isLong.or(startsWithJ); // OR
Predicate<String> notLong = isLong.negate(); // NOT
System.out.println(combined2.test("JavaScript")); // true
System.out.println(notLong.test("Hi")); // true
// UnaryOperator<T>: T -> T (special Function where input and output same type)
UnaryOperator<String> trim = String::trim; // method reference
UnaryOperator<String> lower = String::toLowerCase;
Function<String, String> normalize = trim.andThen(lower);
System.out.println(normalize.apply(" HELLO WORLD ")); // hello world
// BinaryOperator<T>: (T, T) -> T
BinaryOperator<Integer> multiply = (a, b) -> a * b;
System.out.println(multiply.apply(6, 7)); // 42
// === Method References: shorthand for lambdas ===
// Type 1: Static method reference
Function<String, Integer> parse = Integer::parseInt; // same as s -> Integer.parseInt(s)
// Type 2: Instance method reference on a specific instance
String prefix = "Hello";
Predicate<String> startsWith = prefix::startsWith; // unusual but valid
// Type 3: Instance method reference on arbitrary instance of type
Function<String, String> toUpper = String::toUpperCase; // s -> s.toUpperCase()
// Type 4: Constructor reference
Supplier<ArrayList<String>> newList = ArrayList::new;
// Practical use: sorting with lambda vs method reference
List<String> names = new ArrayList<>(Arrays.asList("Charlie", "Alice", "Bob"));
names.sort((a, b) -> a.compareTo(b)); // lambda
names.sort(String::compareTo); // method reference (same thing)
System.out.println(names); // [Alice, Bob, Charlie]
}
}Step 8 — Stream API: Functional Data Processing
The Stream API (Java 8) allows you to process collections of data in a functional, declarative style. Streams are lazy — intermediate operations are only executed when a terminal operation is called.
import java.util.*;
import java.util.stream.*;
public class StreamAPI {
record Employee(String name, String dept, double salary, int age) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "Engineering", 95000, 28),
new Employee("Bob", "Marketing", 65000, 35),
new Employee("Charlie", "Engineering", 85000, 32),
new Employee("Diana", "Engineering", 110000, 29),
new Employee("Eve", "Marketing", 72000, 26),
new Employee("Frank", "HR", 58000, 40)
);
// ============================================
// INTERMEDIATE OPERATIONS (return Stream — lazy)
// ============================================
// filter(): keep elements matching a predicate
List<String> engineers = employees.stream()
.filter(e -> "Engineering".equals(e.dept()))
.map(Employee::name) // map(): transform each element
.collect(Collectors.toList());
System.out.println(engineers); // [Alice, Charlie, Diana]
// sorted(): sort by a field
List<Employee> bySalary = employees.stream()
.sorted(Comparator.comparingDouble(Employee::salary).reversed())
.collect(Collectors.toList());
bySalary.forEach(e -> System.out.printf("%s: $%.0f%n", e.name(), e.salary()));
// distinct(), limit(), skip()
List<Integer> nums = List.of(1, 2, 2, 3, 3, 3, 4, 5);
List<Integer> result = nums.stream()
.distinct() // remove duplicates: [1,2,3,4,5]
.skip(1) // skip first element: [2,3,4,5]
.limit(3) // take only 3: [2,3,4]
.collect(Collectors.toList());
System.out.println(result); // [2, 3, 4]
// flatMap(): flatten nested collections into one stream
List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4), List.of(5));
List<Integer> flat = nested.stream()
.flatMap(Collection::stream) // each inner list becomes a stream, then merged
.collect(Collectors.toList());
System.out.println(flat); // [1, 2, 3, 4, 5]
// ============================================
// TERMINAL OPERATIONS (trigger execution, return non-Stream)
// ============================================
// count()
long engCount = employees.stream()
.filter(e -> "Engineering".equals(e.dept()))
.count();
System.out.println("Engineers: " + engCount); // 3
// reduce(): fold all elements into one value
double totalSalary = employees.stream()
.mapToDouble(Employee::salary)
.sum(); // specialized IntStream/DoubleStream for primitives (avoids boxing)
System.out.printf("Total salary: $%.0f%n", totalSalary);
OptionalDouble avgSalary = employees.stream()
.mapToDouble(Employee::salary)
.average();
avgSalary.ifPresent(a -> System.out.printf("Average salary: $%.0f%n", a));
// min() / max()
Optional<Employee> highestPaid = employees.stream()
.max(Comparator.comparingDouble(Employee::salary));
highestPaid.ifPresent(e -> System.out.println("Highest paid: " + e.name()));
// anyMatch / allMatch / noneMatch
boolean anyOver100k = employees.stream().anyMatch(e -> e.salary() > 100000);
boolean allAdult = employees.stream().allMatch(e -> e.age() >= 18);
boolean noneUnder20 = employees.stream().noneMatch(e -> e.age() < 20);
System.out.println(anyOver100k + " " + allAdult + " " + noneUnder20);
// findFirst() / findAny()
Optional<Employee> firstEng = employees.stream()
.filter(e -> "Engineering".equals(e.dept()))
.findFirst();
firstEng.ifPresent(e -> System.out.println("First engineer: " + e.name()));
// ============================================
// COLLECTORS: grouping, partitioning, joining
// ============================================
// groupingBy: group employees by department
Map<String, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::dept));
byDept.forEach((dept, emps) ->
System.out.println(dept + ": " + emps.stream().map(Employee::name).toList()));
// groupingBy with downstream collector: count per department
Map<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));
System.out.println(countByDept); // {Engineering=3, Marketing=2, HR=1}
// groupingBy with average salary per dept
Map<String, Double> avgByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::dept,
Collectors.averagingDouble(Employee::salary)));
System.out.println(avgByDept);
// partitioningBy: split into two groups (true/false)
Map<Boolean, List<Employee>> partition = employees.stream()
.collect(Collectors.partitioningBy(e -> e.salary() > 80000));
System.out.println("High earners: " +
partition.get(true).stream().map(Employee::name).toList());
// joining: concatenate strings
String namesList = employees.stream()
.map(Employee::name)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(namesList); // [Alice, Bob, Charlie, Diana, Eve, Frank]
// toMap: convert to map
Map<String, Double> nameSalaryMap = employees.stream()
.collect(Collectors.toMap(Employee::name, Employee::salary));
System.out.println(nameSalaryMap.get("Alice")); // 95000.0
}
}Step 9 — Optional: Null-Safe Values
Optional<T> is a container that may or may not contain a non-null value. It forces you to handle the missing-value case explicitly, eliminating NullPointerExceptions when used at API boundaries.
import java.util.Optional;
public class OptionalExample {
// Return Optional instead of null — communicates clearly that value may be absent
static Optional<String> findUserById(int id) {
if (id == 1) return Optional.of("Kuldeep"); // value is present
return Optional.empty(); // no value
}
static Optional<String> getEmailForUser(String name) {
if ("Kuldeep".equals(name)) return Optional.of("kuldeep@example.com");
return Optional.empty();
}
public static void main(String[] args) {
// Creating Optional
Optional<String> present = Optional.of("Hello"); // must be non-null
Optional<String> empty = Optional.empty(); // no value
Optional<String> nullable = Optional.ofNullable(null); // safe: wraps null as empty
// Checking and retrieving
System.out.println(present.isPresent()); // true
System.out.println(empty.isPresent()); // false
System.out.println(present.isEmpty()); // false (Java 11+)
// get(): unsafe — throws NoSuchElementException if empty. Avoid in production.
// System.out.println(empty.get()); // throws!
// orElse(): provide a default value
String value = empty.orElse("default");
System.out.println(value); // default
// orElseGet(): lazy default — supplier called only if empty (preferred for expensive operations)
String value2 = empty.orElseGet(() -> "computed default");
// orElseThrow(): throw if empty (Java 10+)
// empty.orElseThrow(() -> new RuntimeException("Not found"));
// ifPresent(): action only if value exists (like Consumer)
present.ifPresent(v -> System.out.println("Found: " + v)); // Found: Hello
// ifPresentOrElse() (Java 9+)
empty.ifPresentOrElse(
v -> System.out.println("Value: " + v),
() -> System.out.println("No value found")
); // No value found
// map(): transform value if present (still returns Optional)
Optional<Integer> length = present.map(String::length);
System.out.println(length); // Optional[5]
// filter(): keep value only if condition is met
Optional<String> filtered = present.filter(s -> s.length() > 3);
System.out.println(filtered); // Optional[Hello]
// flatMap(): when the mapper itself returns an Optional (avoids Optional<Optional<T>>)
Optional<String> email = findUserById(1)
.flatMap(OptionalExample::getEmailForUser);
System.out.println(email.orElse("No email")); // kuldeep@example.com
Optional<String> noEmail = findUserById(99)
.flatMap(OptionalExample::getEmailForUser);
System.out.println(noEmail.orElse("No email")); // No email
// or() (Java 9+): provide alternative Optional if empty
Optional<String> alternative = empty.or(() -> Optional.of("fallback"));
System.out.println(alternative.get()); // fallback
}
}