Java Collections Framework

Series: Java Core for Beginners Lecture: 08 of 12 Topics: Collection hierarchy · ArrayList vs LinkedList · HashSet & TreeSet · HashMap & TreeMap · Iterating collections · Choosing the right collection


Why Collections?

In Lecture 7 you worked with arrays — Java’s built-in fixed-size sequence. Arrays are fast and memory-efficient, but they have a fundamental limitation: the size is fixed at creation. If you need to add a tenth element to a nine-element array, you must allocate a new larger array, copy all elements, and discard the old one — manually.

The Java Collections Framework (JCF) solves this and much more. It provides a rich set of data structures — lists, sets, queues, maps — each with different performance characteristics and behavioral guarantees, all backed by carefully engineered implementations. Every collection grows and shrinks automatically.

Beyond resizability, collections give you:

  • Rich APIs — sorting, searching, filtering, converting to arrays
  • Interoperability — all collections implement common interfaces, so code written against List works with any List implementation
  • Generics — type-safe containers that eliminate casting and catch type errors at compile time
  • Null handling and ordering guarantees that vary by implementation

Almost all real Java programs use collections far more than raw arrays. Mastering the JCF is a prerequisite for effective Java development.


The Collection Hierarchy

The JCF is built around a hierarchy of interfaces. Understanding which interface each class implements tells you what it can do.

java.lang.Iterable
    └── java.util.Collection
            ├── List          (ordered, indexed, duplicates allowed)
            │     ├── ArrayList
            │     └── LinkedList
            ├── Set           (no duplicates)
            │     ├── HashSet
            │     ├── LinkedHashSet
            │     └── TreeSet
            └── Queue         (FIFO access)
                  └── LinkedList (also implements Deque)

java.util.Map  (key-value pairs — NOT a Collection)
      ├── HashMap
      ├── LinkedHashMap
      └── TreeMap

Key interfaces to know:

  • Collection — the root interface. Defines add(), remove(), contains(), size(), isEmpty(), clear(), and iteration.
  • List — an ordered collection where elements have indices. Allows duplicates. Defines index-based access: get(i), set(i, e), add(i, e), remove(i).
  • Set — a collection that contains no duplicate elements. Does not guarantee order (unless using LinkedHashSet or TreeSet).
  • Map — maps keys to values. Each key maps to exactly one value. Not a Collection, but part of the framework.

Always program to the interface, not the implementation. Declare variables with the interface type:

// Good — code depends only on List behavior, not ArrayList specifics
List<String> names = new ArrayList<>();

// Avoid — ties you to ArrayList, harder to swap implementations later
ArrayList<String> names = new ArrayList<>();

ArrayList

ArrayList is the most commonly used collection in Java. It is a resizable array — it wraps a regular array internally but automatically grows when capacity is exceeded (typically doubling in size).

Creating an ArrayList

import java.util.ArrayList;
import java.util.List;

List<String> names = new ArrayList<>();           // empty list
List<Integer> scores = new ArrayList<>(50);       // initial capacity hint — no elements yet
List<String> copy = new ArrayList<>(otherList);   // copy constructor

The type parameter (e.g., ) makes the list generic — it can only hold String objects. The compiler enforces this at compile time, eliminating the need for casts when retrieving elements.

Core operations

List<String> fruits = new ArrayList<>();

// Adding elements
fruits.add("Apple");                   // adds to the end
fruits.add("Banana");
fruits.add("Cherry");
fruits.add(1, "Blueberry");           // inserts at index 1, shifts others right

System.out.println(fruits);           // [Apple, Blueberry, Banana, Cherry]

// Accessing elements
String first = fruits.get(0);         // "Apple"
String last  = fruits.get(fruits.size() - 1);   // "Cherry"

// Modifying elements
fruits.set(2, "Mango");               // replaces element at index 2
System.out.println(fruits);           // [Apple, Blueberry, Mango, Cherry]

// Size and emptiness
System.out.println(fruits.size());    // 4
System.out.println(fruits.isEmpty()); // false

// Checking membership
System.out.println(fruits.contains("Apple"));   // true
System.out.println(fruits.indexOf("Mango"));    // 2 — first occurrence index
System.out.println(fruits.indexOf("Grape"));    // -1 — not found

// Removing elements
fruits.remove("Blueberry");           // removes by value (first occurrence)
fruits.remove(0);                     // removes by index
System.out.println(fruits);           // [Mango, Cherry]

// Clearing
fruits.clear();
System.out.println(fruits.isEmpty()); // true

Important: removing by index vs by value

When your list holds Integer objects, calling remove(2) removes the element at index 2, not the element with the value 2. To remove by value, wrap it:

List<Integer> numbers = new ArrayList<>(List.of(10, 20, 30, 20, 40));

numbers.remove(2);                        // removes at index 2 → [10, 20, 20, 40]
numbers.remove(Integer.valueOf(20));      // removes first occurrence of value 20 → [10, 20, 40]

Adding all elements of another collection

List<String> a = new ArrayList<>(List.of("Alice", "Bob"));
List<String> b = new ArrayList<>(List.of("Charlie", "Dave"));

a.addAll(b);                          // appends all of b to a
System.out.println(a);                // [Alice, Bob, Charlie, Dave]

a.addAll(1, b);                       // inserts b starting at index 1

Sorting

List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));

Collections.sort(names);                              // natural order
System.out.println(names);                            // [Alice, Bob, Charlie]

names.sort(Comparator.reverseOrder());                // reverse order
System.out.println(names);                            // [Charlie, Bob, Alice]

names.sort(Comparator.comparingInt(String::length));  // by length
System.out.println(names);                            // [Bob, Alice, Charlie]

Converting between arrays and lists

// Array → List
String[] arr = {"Alice", "Bob", "Charlie"};
List<String> list = new ArrayList<>(Arrays.asList(arr));

// List → Array
String[] back = list.toArray(new String[0]);

Performance characteristics of ArrayList

Operation Time complexity Notes
get(i) O(1) Direct index access
set(i, e) O(1) Direct index access
add(e) (end) O(1) amortized Occasional resizing
add(i, e) (middle) O(n) Shifts elements right
remove(i) (middle) O(n) Shifts elements left
contains(e) O(n) Linear search

LinkedList

LinkedList implements both List and Deque (double-ended queue). Internally it is a doubly linked list — each element holds a reference to the previous and next elements rather than being stored in a contiguous array.

import java.util.LinkedList;

LinkedList<String> queue = new LinkedList<>();

// Standard List operations work the same
queue.add("Alice");
queue.add("Bob");
queue.add("Charlie");

// Deque operations — efficient at both ends
queue.addFirst("Zara");           // inserts at the front
queue.addLast("Dave");            // appends to the back
System.out.println(queue);        // [Zara, Alice, Bob, Charlie, Dave]

String first = queue.peekFirst(); // "Zara" — looks without removing
String last  = queue.peekLast();  // "Dave"

queue.pollFirst();                // removes and returns the first element
queue.pollLast();                 // removes and returns the last element
System.out.println(queue);        // [Alice, Bob, Charlie]

LinkedList as a stack

LinkedList<Integer> stack = new LinkedList<>();
stack.push(10);   // same as addFirst
stack.push(20);
stack.push(30);
System.out.println(stack);       // [30, 20, 10]

int top = stack.pop();           // same as removeFirst — returns 30
System.out.println(stack);       // [20, 10]
System.out.println(stack.peek()); // 20 — look without removing

Performance characteristics of LinkedList

Operation Time complexity Notes
get(i) O(n) Must traverse from head/tail
add(e) (end) O(1) Direct tail pointer update
add(i, e) (middle) O(n) Traversal to position, then O(1) insert
addFirst/addLast O(1) Direct head/tail update
removeFirst/removeLast O(1) Direct head/tail update
contains(e) O(n) Linear traversal

ArrayList vs LinkedList — Choosing Between Them

This is one of the most common questions in Java. The practical answer for most applications is simple: use ArrayList by default.

Criterion ArrayList LinkedList
Random access get(i) O(1) — fast O(n) — slow
Append to end O(1) amortized O(1)
Insert/delete at middle O(n) — slow O(n) — fast at node, slow to find it
Insert/delete at front O(n) — slow O(1) — fast
Memory overhead Low (one array) High (two pointers per node)
Cache performance Excellent (contiguous memory) Poor (scattered nodes)

Choose ArrayList when:

  • You access elements by index frequently
  • You mostly add to the end
  • Memory efficiency matters
  • Cache performance matters (almost always)

Choose LinkedList when:

  • You need a queue or deque and frequently add/remove from both ends
  • You are implementing a stack, queue, or deque explicitly

In practice, even for frequent middle insertions, ArrayList often outperforms LinkedList on modern hardware because of CPU cache effects — sequential memory access is dramatically faster than pointer-chasing through scattered nodes.


HashSet and TreeSet

A Set is a collection that does not allow duplicate elements. Adding an element that already exists is silently ignored (returns false). Sets are the right tool when you need to track membership, remove duplicates, or perform mathematical set operations.

HashSet

HashSet is backed by a hash table. It offers O(1) average-time performance for add, remove, and contains. It makes no guarantee about iteration order.

import java.util.HashSet;
import java.util.Set;

Set<String> tags = new HashSet<>();

tags.add("java");
tags.add("programming");
tags.add("beginner");
tags.add("java");           // duplicate — silently ignored
tags.add("programming");    // duplicate — silently ignored

System.out.println(tags.size());          // 3
System.out.println(tags.contains("java")); // true
System.out.println(tags);                 // [programming, beginner, java] — order varies

tags.remove("beginner");
System.out.println(tags);                 // [programming, java]

Removing duplicates from a list

One of the most common uses of HashSet:

List<Integer> withDuplicates = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5));
Set<Integer>  unique          = new HashSet<>(withDuplicates);   // removes duplicates
List<Integer> deduplicated    = new ArrayList<>(unique);         // back to list if needed

System.out.println(withDuplicates); // [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
System.out.println(unique);         // [1, 2, 3, 4, 5, 6, 9] — order not guaranteed

Set operations

Set<String> setA = new HashSet<>(Set.of("Alice", "Bob", "Charlie"));
Set<String> setB = new HashSet<>(Set.of("Bob", "Charlie", "Dave"));

// Union: all elements from both sets
Set<String> union = new HashSet<>(setA);
union.addAll(setB);
System.out.println(union);   // [Alice, Bob, Charlie, Dave]

// Intersection: elements in both sets
Set<String> intersection = new HashSet<>(setA);
intersection.retainAll(setB);
System.out.println(intersection);   // [Bob, Charlie]

// Difference: elements in A but not in B
Set<String> difference = new HashSet<>(setA);
difference.removeAll(setB);
System.out.println(difference);   // [Alice]

HashSet and equals/hashCode

HashSet uses hashCode() to determine the bucket and equals() to check for true equality. For HashSet to work correctly with custom objects, you must override both equals() and hashCode() consistently (as discussed in Lecture 5). This is why Java’s built-in types like String, Integer, and LocalDate work correctly in sets — they all properly implement both methods.

Set<String> set = new HashSet<>();
set.add("hello");
set.add("hello");   // same content → equals() returns true → not added
System.out.println(set.size());   // 1

TreeSet

TreeSet is backed by a red-black tree. It stores elements in sorted order (natural ordering for types like String and Integer, or a custom Comparator) and offers O(log n) performance for add, remove, and contains.

import java.util.TreeSet;

Set<Integer> sorted = new TreeSet<>();
sorted.add(5);
sorted.add(1);
sorted.add(3);
sorted.add(2);
sorted.add(4);

System.out.println(sorted);   // [1, 2, 3, 4, 5] — always sorted

// TreeSet-specific navigation methods
TreeSet<Integer> ts = new TreeSet<>(sorted);
System.out.println(ts.first());         // 1
System.out.println(ts.last());          // 5
System.out.println(ts.floor(3));        // 3 — largest element ≤ 3
System.out.println(ts.ceiling(3));      // 3 — smallest element ≥ 3
System.out.println(ts.lower(3));        // 2 — largest element < 3
System.out.println(ts.higher(3));       // 4 — smallest element > 3
System.out.println(ts.headSet(3));      // [1, 2] — elements < 3
System.out.println(ts.tailSet(3));      // [3, 4, 5] — elements ≥ 3
System.out.println(ts.subSet(2, 5));    // [2, 3, 4] — [2, 5)

LinkedHashSet

A middle ground between HashSet and TreeSet: it maintains insertion order while still offering O(1) average performance. Use it when you need unique elements in the order they were added:

Set<String> ordered = new LinkedHashSet<>();
ordered.add("banana");
ordered.add("apple");
ordered.add("cherry");
ordered.add("apple");   // duplicate — ignored

System.out.println(ordered);   // [banana, apple, cherry] — insertion order preserved

HashSet vs TreeSet vs LinkedHashSet

HashSet TreeSet LinkedHashSet
Order None Sorted Insertion order
Performance O(1) average O(log n) O(1) average
Null elements One allowed Not allowed One allowed
Use when Fastest lookup, order irrelevant Sorted iteration needed Insertion order needed

HashMap and TreeMap

A Map stores key-value pairs. Each key maps to exactly one value. Keys must be unique — adding a second entry with an existing key replaces the value.

HashMap

HashMap is backed by a hash table. It offers O(1) average-time performance for put, get, and remove. Order is not guaranteed.

import java.util.HashMap;
import java.util.Map;

Map<String, Integer> scores = new HashMap<>();

// Adding entries
scores.put("Alice",   95);
scores.put("Bob",     82);
scores.put("Charlie", 67);
scores.put("Alice",   98);   // replaces Alice's previous score

System.out.println(scores);              // {Bob=82, Alice=98, Charlie=67} — order varies
System.out.println(scores.size());       // 3

// Retrieving values
System.out.println(scores.get("Alice")); // 98
System.out.println(scores.get("Dave"));  // null — key not present

// Safe retrieval with default
System.out.println(scores.getOrDefault("Dave", 0));   // 0

// Checking keys and values
System.out.println(scores.containsKey("Bob"));        // true
System.out.println(scores.containsValue(82));         // true

// Removing entries
scores.remove("Charlie");
System.out.println(scores);   // {Bob=82, Alice=98}

// Conditional put — only adds if key is absent
scores.putIfAbsent("Charlie", 70);
System.out.println(scores.get("Charlie"));   // 70
scores.putIfAbsent("Charlie", 99);           // Charlie already exists — no change
System.out.println(scores.get("Charlie"));   // 70

Iterating a HashMap

Map<String, Integer> scores = new HashMap<>(Map.of("Alice", 95, "Bob", 82, "Charlie", 67));

// Iterate over entries (most common)
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " → " + entry.getValue());
}

// Iterate over keys only
for (String name : scores.keySet()) {
    System.out.println(name);
}

// Iterate over values only
for (int score : scores.values()) {
    System.out.println(score);
}

// Modern forEach with lambda
scores.forEach((name, score) ->
    System.out.printf("%-10s: %d%n", name, score)
);

Common HashMap patterns

Counting occurrences (frequency map):

String text = "the quick brown fox jumps over the lazy dog";
Map<Character, Integer> frequency = new HashMap<>();

for (char c : text.toCharArray()) {
    if (c != ' ') {
        frequency.put(c, frequency.getOrDefault(c, 0) + 1);
    }
}

System.out.println(frequency.get('o'));   // 4

Grouping elements:

List<String> words = List.of("ant", "bear", "cat", "ape", "bat", "cobra");
Map<Character, List<String>> byFirstLetter = new HashMap<>();

for (String word : words) {
    char key = word.charAt(0);
    byFirstLetter.computeIfAbsent(key, k -> new ArrayList<>()).add(word);
}

System.out.println(byFirstLetter);
// {a=[ant, ape], b=[bear, bat], c=[cat, cobra]}

computeIfAbsent(key, mappingFunction) creates the value (here, a new ArrayList) only if the key is absent, then returns it. This eliminates the null check boilerplate.

Merging counts:

Map<String, Integer> sales = new HashMap<>();
String[] items = {"apple", "banana", "apple", "cherry", "banana", "apple"};

for (String item : items) {
    sales.merge(item, 1, Integer::sum);
    // If key absent: put(item, 1)
    // If key present: put(item, existingValue + 1)
}

System.out.println(sales);   // {banana=2, cherry=1, apple=3}

TreeMap

TreeMap stores entries sorted by key using natural ordering or a custom Comparator. It offers O(log n) performance for put, get, and remove.

import java.util.TreeMap;

Map<String, Integer> sorted = new TreeMap<>();
sorted.put("Charlie", 67);
sorted.put("Alice",   95);
sorted.put("Bob",     82);

System.out.println(sorted);   // {Alice=95, Bob=82, Charlie=67} — always sorted by key

// TreeMap-specific navigation
TreeMap<String, Integer> tm = new TreeMap<>(sorted);
System.out.println(tm.firstKey());              // "Alice"
System.out.println(tm.lastKey());               // "Charlie"
System.out.println(tm.floorKey("B"));           // "Alice" — largest key ≤ "B"
System.out.println(tm.ceilingKey("B"));         // "Bob" — smallest key ≥ "B"
System.out.println(tm.headMap("Charlie"));      // {Alice=95, Bob=82} — keys < "Charlie"
System.out.println(tm.tailMap("Bob"));          // {Bob=82, Charlie=67} — keys ≥ "Bob"

LinkedHashMap

Like LinkedHashSet, LinkedHashMap preserves insertion order:

Map<String, String> config = new LinkedHashMap<>();
config.put("host",     "localhost");
config.put("port",     "5432");
config.put("database", "mydb");
config.put("username", "admin");

config.forEach((k, v) -> System.out.println(k + "=" + v));
// host=localhost
// port=5432
// database=mydb
// username=admin

HashMap vs TreeMap vs LinkedHashMap

HashMap TreeMap LinkedHashMap
Key order None Sorted by key Insertion order
Performance O(1) average O(log n) O(1) average
Null keys One allowed Not allowed One allowed
Use when Fastest lookup Sorted key iteration Insertion order needed

Iterating Collections

Java offers several ways to iterate over collections. Knowing which to use in each context makes your code cleaner and more expressive.

Enhanced for loop (for-each)

The cleanest option for simple iteration. Works with any Iterable:

List<String> names = List.of("Alice", "Bob", "Charlie");

for (String name : names) {
    System.out.println(name);
}

Limitation: you cannot modify the collection while iterating (throws ConcurrentModificationException), and you cannot access the current index.

Iterator

Iterator gives explicit control. Use it when you need to safely remove elements during iteration:

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie", "Dave"));
Iterator<String> it = names.iterator();

while (it.hasNext()) {
    String name = it.next();
    if (name.startsWith("C")) {
        it.remove();   // safe removal during iteration
    }
}

System.out.println(names);   // [Alice, Bob, Dave]

Never call list.remove() inside a for-each loop — it causes ConcurrentModificationException. Use iterator.remove() instead.

ListIterator

ListIterator extends Iterator with bidirectional traversal and the ability to add and set elements during iteration:

List<String> names = new ArrayList<>(List.of("alice", "bob", "charlie"));
ListIterator<String> lit = names.listIterator();

while (lit.hasNext()) {
    String name = lit.next();
    lit.set(name.substring(0, 1).toUpperCase() + name.substring(1));   // capitalize
}

System.out.println(names);   // [Alice, Bob, Charlie]

forEach with lambda

The modern, functional-style iteration. Clean for simple operations:

List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));

// Or with a method reference
names.forEach(System.out::println);

// Map
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 82);
scores.forEach((name, score) -> System.out.println(name + ": " + score));

removeIf

Removes all elements matching a predicate — cleaner than using an iterator for filtering:

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
numbers.removeIf(n -> n % 2 == 0);   // remove all even numbers
System.out.println(numbers);          // [1, 3, 5, 7, 9]

replaceAll

Replaces each element with the result of applying a function — cleaner than an indexed loop for transformations:

List<String> names = new ArrayList<>(List.of("alice", "bob", "charlie"));
names.replaceAll(String::toUpperCase);
System.out.println(names);   // [ALICE, BOB, CHARLIE]

Utility Methods — Collections and List.of

Collections utility class

java.util.Collections provides static utility methods for working with collections:

import java.util.Collections;

List<Integer> numbers = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));

Collections.sort(numbers);                          // [1, 1, 2, 3, 4, 5, 6, 9]
Collections.reverse(numbers);                       // [9, 6, 5, 4, 3, 2, 1, 1]
Collections.shuffle(numbers);                       // random order
System.out.println(Collections.min(numbers));       // smallest element
System.out.println(Collections.max(numbers));       // largest element
System.out.println(Collections.frequency(numbers, 1)); // count occurrences of 1

Collections.sort(numbers);
int idx = Collections.binarySearch(numbers, 5);    // index of 5 (list must be sorted)

Collections.fill(numbers, 0);                      // set all elements to 0
Collections.swap(numbers, 0, numbers.size() - 1); // swap first and last

// Unmodifiable view — wraps a modifiable list, throws on mutation attempts
List<Integer> immutable = Collections.unmodifiableList(numbers);

Factory methods — List.of, Set.of, Map.of (Java 9+)

Since Java 9, you can create immutable collections concisely:

// Immutable List — no add, remove, or set allowed
List<String> names = List.of("Alice", "Bob", "Charlie");

// Immutable Set
Set<String> tags = Set.of("java", "programming", "oop");

// Immutable Map
Map<String, Integer> codes = Map.of(
    "Alice", 101,
    "Bob",   102,
    "Charlie", 103
);

// Map with more than 10 entries — use Map.ofEntries
Map<String, Integer> large = Map.ofEntries(
    Map.entry("Alice",   101),
    Map.entry("Bob",     102),
    Map.entry("Charlie", 103),
    Map.entry("Dave",    104)
    // ... up to any number of entries
);

These factory-created collections are truly immutable — any attempt to add, remove, or modify throws UnsupportedOperationException. They are also slightly more memory-efficient than their mutable counterparts.

To create a mutable collection from an immutable one, wrap it:

List<String> mutable = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
mutable.add("Dave");   // fine — mutable copy

Choosing the Right Collection

The most important design skill with collections is picking the right one. Use this guide:

Do you need key-value pairs?

Yes → Use a Map:

  • Order does not matter → HashMap (fastest, O(1))
  • Sorted by key → TreeMap (O(log n))
  • Insertion order → LinkedHashMap (O(1))

No → Use a Collection:

Do you need unique elements?

Yes → Use a Set:

  • Order does not matter → HashSet (fastest, O(1))
  • Sorted order → TreeSet (O(log n))
  • Insertion order → LinkedHashSet (O(1))

No → Use a List:

  • General purpose / random access → ArrayList (almost always)
  • Frequent insert/delete at both ends → LinkedList (as a Deque)

Quick decision table

Need Best choice
Ordered list with duplicates ArrayList
Key → value lookup HashMap
Unique elements, no order HashSet
Unique elements, sorted TreeSet
Sorted key → value TreeMap
Queue (FIFO) LinkedList as Queue / ArrayDeque
Stack (LIFO) ArrayDeque (or LinkedList)
Insertion order, unique LinkedHashSet
Insertion order, key-value LinkedHashMap
Immutable list List.of(...)
Immutable set Set.of(...)
Immutable map Map.of(...)

A note on null

  • HashMap, HashSet, ArrayList, LinkedList, LinkedHashMap, LinkedHashSet — all allow null values (and one null key for maps/sets)
  • TreeMap and TreeSet — do not allow null (the comparator would throw NullPointerException)
  • List.of(), Set.of(), Map.of() — do not allow null (throws NullPointerException at creation)

Summary

  • The Java Collections Framework provides List, Set, and Map interfaces with multiple implementations, each with different performance characteristics.
  • Program to the interface: declare variables as List, Set, or Map, not as ArrayList, HashSet, etc.
  • ArrayList is a resizable array. O(1) random access, O(n) middle insert/delete. Use it by default for ordered sequences.
  • LinkedList is a doubly linked list implementing both List and Deque. O(1) at both ends, O(n) random access. Use it when you need a queue or deque.
  • HashSet stores unique elements with no ordering, O(1) average. TreeSet stores them sorted, O(log n). LinkedHashSet preserves insertion order.
  • HashMap stores key-value pairs with no ordering, O(1) average. TreeMap stores them sorted by key. LinkedHashMap preserves insertion order.
  • Use Iterator.remove() — never list.remove() — to remove elements during iteration.
  • removeIf() and replaceAll() are clean, modern alternatives to manual iteration for filtering and transformation.
  • List.of(), Set.of(), Map.of() create immutable collections. Wrap them in a mutable constructor if you need to add elements.
  • The Collections utility class provides sort(), reverse(), shuffle(), min(), max(), frequency(), binarySearch(), and more.

Exercises

Exercise 1 — ArrayList word list Read the sentence "to be or not to be that is the question" and split it into words. Store the words in an ArrayList. Then: (a) sort alphabetically, (b) remove duplicates by converting to a LinkedHashSet and back, (c) find the longest word, (d) count how many words have more than 2 characters. Print results for each step.

Exercise 2 — HashSet duplicate detection Write a method List findDuplicates(List input) that returns a list of all values that appear more than once in the input. Use a HashSet to track seen values and a second HashSet to avoid reporting the same duplicate twice. Test with [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].

Exercise 3 — Frequency map Write a program that takes the text "to be or not to be that is the question", builds a HashMap counting each word’s frequency, then: (a) prints all words and their counts sorted by frequency (most frequent first), (b) prints the top 3 most frequent words, (c) prints words that appear exactly once.

Exercise 4 — Phone book with TreeMap Implement a simple phone book using TreeMap (name → phone number). Provide methods: add(name, phone), lookup(name), delete(name), listAll() (prints entries in alphabetical order), and search(prefix) (returns all names starting with the given prefix — use TreeMap.subMap() or iterate). Demonstrate all operations in main.

Exercise 5 — Grouping and aggregation You have a list of transactions, each with a category (String) and amount (double). Model this as a simple class Transaction. Build a Map> grouping transactions by category using computeIfAbsent. Then compute and print the total amount per category, sorted by total (highest first). Use the categories: "Food", "Transport", "Entertainment", "Utilities".

Exercise 6 — Inventory manager Build an InventoryManager class backed by a HashMap (product name → quantity). Implement: addStock(product, quantity), sellStock(product, quantity) (throws if insufficient stock), getQuantity(product), getLowStockItems(int threshold) returning a sorted list of products with quantity below the threshold, and printInventory() printing items in alphabetical order. Write a main that performs a sequence of operations and prints the inventory state after each.


Up next: Lecture 9 — Generics — where you will understand how Java’s type parameter system works under the hood, write your own generic classes and methods, and master wildcards to write flexible, reusable code.

Leave a Reply

Your email address will not be published. Required fields are marked *