PYnative

Python Programming

  • Learn Python ▼
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises ▼
    • Python Exercises
    • C++ Exercises
    • C Programming Exercises
    • Java Exercises
    • C# Exercises
  • Quizzes
  • Online Compilers▼
    • Online Python Code Editor
    • Online C Compiler
    • Online C++ Compiler
    • Online Java Compiler
    • Online C# Compiler
Home » Java Exercises » Java HashMap and TreeMap Exercises: 25 Coding Problems with Solutions

Java HashMap and TreeMap Exercises: 25 Coding Problems with Solutions

Updated on: July 9, 2026 | Leave a Comment

This collection of 25 Java exercises covers the full Map family: HashMap, TreeMap, LinkedHashMap, and how they compare in ordering, performance, and use case.

You’ll start with basic CRUD operations, iteration styles, and useful shortcut methods like merge() and computeIfAbsent(), then move into TreeMap-specific navigation methods (floorKey(), ceilingKey(), subMap(), descendingMap()), LinkedHashMap‘s insertion and access ordering (including a hand-built LRU cache), grouping and sorting with the Stream API, and closing out with a look at why plain HashMap isn’t thread-safe.

Each exercise includes a Problem Statement, Purpose, Hint, and a Solution with Explanation, so the reasoning behind each method choice is just as clear as the code itself.

  • Also, See: Java Exercises with over 20+ topic-wise sets and 575+ coding questions to practice.
  • Practice questions using our Online Java Compiler
+ Table of Contents (25 Exercises)

Table of contents

  • Exercise 1: HashMap Basic Operations
  • Exercise 2: Iterating a HashMap
  • Exercise 3: Word Frequency Counter
  • Exercise 4: Merging Two HashMaps
  • Exercise 5: Default Value with computeIfAbsent()
  • Exercise 6: hashCode() and equals() in Custom Keys
  • Exercise 7: Finding Max and Min Values
  • Exercise 8: Inverting a Map
  • Exercise 9: TreeMap Natural Ordering
  • Exercise 10: TreeMap with a Custom Comparator
  • Exercise 11: First and Last Entry in a TreeMap
  • Exercise 12: floorKey() and ceilingKey()
  • Exercise 13: lowerKey() and higherKey()
  • Exercise 14: Extracting a Range with subMap()
  • Exercise 15: Splitting a Map with headMap() and tailMap()
  • Exercise 16: Reversing a TreeMap with descendingMap()
  • Exercise 17: HashMap vs LinkedHashMap Order
  • Exercise 18: LinkedHashMap Access Order
  • Exercise 19: Building an LRU Cache
  • Exercise 20: First and Last Key in LinkedHashMap
  • Exercise 21: Updating a Value Without Changing Order
  • Exercise 22: Grouping Objects with Streams
  • Exercise 23: Converting HashMap to TreeMap
  • Exercise 24: Sorting a Map by Value
  • Exercise 25: Thread Safety: HashMap vs ConcurrentHashMap

Exercise 1: HashMap Basic Operations

Problem Statement: Create a HashMap that stores the names of 5 cities (Keys) and their populations (Values). Practice adding, retrieving, updating, and removing entries.

Purpose: This exercise helps you practice the four fundamental operations on a HashMap, adding new entries, reading a value by its key, updating an existing value, and removing an entry entirely.

Given Input: Cities: Delhi, Mumbai, Chennai, Kolkata, Pune with their respective populations

Population of Mumbai: 20411000
Updated population of Pune: 7500000
After removing Kolkata: {Delhi=32900000, Mumbai=20411000, Chennai=10971000, Pune=7500000}
▼ Hint
  • Use put(key, value) five times to add the initial cities and populations.
  • Use get(key) to retrieve a single population by city name.
  • Call put() again with an existing key to overwrite its value, which is how updates work in a HashMap.
  • Use remove(key) to delete an entry completely.
▼ Solution & Explanation
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> cityPopulation = new HashMap<>();
        cityPopulation.put("Delhi", 32900000);
        cityPopulation.put("Mumbai", 20411000);
        cityPopulation.put("Chennai", 10971000);
        cityPopulation.put("Kolkata", 14850000);
        cityPopulation.put("Pune", 7400000);

        System.out.println("Population of Mumbai: " + cityPopulation.get("Mumbai"));

        cityPopulation.put("Pune", 7500000);
        System.out.println("Updated population of Pune: " + cityPopulation.get("Pune"));

        cityPopulation.remove("Kolkata");
        System.out.println("After removing Kolkata: " + cityPopulation);
    }
}Code language: Java (java)

Explanation:

  • cityPopulation.put("Delhi", 32900000): Adds a new key-value pair to the map, or overwrites the value if the key already exists.
  • cityPopulation.get("Mumbai"): Looks up the value associated with the given key and returns null if the key is not present.
  • cityPopulation.put("Pune", 7500000): Since "Pune" already exists as a key, this replaces its old value instead of creating a duplicate entry.
  • cityPopulation.remove("Kolkata"): Deletes the entry for "Kolkata" entirely, reducing the map’s size by one.

Exercise 2: Iterating a HashMap

Problem Statement: Write a program that iterates through a HashMap using four different methods: entrySet(), keySet(), the forEach() loop, and an Iterator.

Purpose: This exercise surveys the different ways Java allows you to traverse a map, helping you recognize each style in existing code and choose the most appropriate one for a given situation.

Given Input: Map: {"A"=1, "B"=2, "C"=3}

Using entrySet: A=1
Using entrySet: B=2
Using entrySet: C=3
Using keySet: A -> 1
Using keySet: B -> 2
Using keySet: C -> 3
Using forEach: A = 1
Using forEach: B = 2
Using forEach: C = 3
Using Iterator: A=1
Using Iterator: B=2
Using Iterator: C=3
▼ Hint
  • Loop over map.entrySet() with a for-each loop, reading both the key and value directly from each Map.Entry.
  • Loop over map.keySet(), then call map.get(key) inside the loop to fetch each value separately.
  • Call map.forEach((key, value) -> ...) and pass a lambda that receives both parameters directly.
  • Get an Iterator from map.entrySet().iterator() and use hasNext() and next() to walk through the entries manually.
▼ Solution & Explanation
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);

        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println("Using entrySet: " + entry.getKey() + "=" + entry.getValue());
        }

        for (String key : map.keySet()) {
            System.out.println("Using keySet: " + key + " -> " + map.get(key));
        }

        map.forEach((key, value) -> System.out.println("Using forEach: " + key + " = " + value));

        Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Integer> entry = iterator.next();
            System.out.println("Using Iterator: " + entry.getKey() + "=" + entry.getValue());
        }
    }
}Code language: Java (java)

Explanation:

  • map.entrySet(): Returns a view of all key-value pairs together as Map.Entry objects, avoiding a second lookup to fetch the value.
  • map.keySet(): Returns only the keys, requiring a separate get() call inside the loop if the value is also needed.
  • map.forEach((key, value) -> ...): Accepts a lambda expression that receives each key and value pair directly, without an explicit loop construct.
  • map.entrySet().iterator(): Gives explicit control over traversal, which is useful when entries need to be removed safely during iteration using iterator.remove().

Exercise 3: Word Frequency Counter

Problem Statement: Given a long paragraph of text, write a program that counts the frequency of each unique word using a HashMap and prints the results.

Purpose: This exercise practices a very common map pattern, accumulating counts per key while scanning through a data source, which is the foundation of frequency analysis and basic text processing.

Given Input: String text = "the quick fox jumps over the lazy fox the fox runs";

the: 3
quick: 1
fox: 3
jumps: 1
over: 1
lazy: 1
runs: 1
▼ Hint
  • Split the text into individual words using text.split(" ").
  • For each word, use map.getOrDefault(word, 0) to read the current count, defaulting to 0 if the word has not been seen yet.
  • Add 1 to that value and store it back into the map using put().
▼ Solution & Explanation
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        String text = "the quick fox jumps over the lazy fox the fox runs";
        String[] words = text.split(" ");

        HashMap<String, Integer> wordCount = new HashMap<>();
        for (String word : words) {
            wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
        }

        for (Map.Entry<String, Integer> entry : wordCount.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}Code language: Java (java)

Explanation:

  • text.split(" "): Breaks the sentence into an array of individual words based on spaces.
  • wordCount.getOrDefault(word, 0): Returns the word’s current count if it exists, or 0 if this is the first time the word has appeared, avoiding a separate containsKey() check.
  • wordCount.put(word, ... + 1): Writes the incremented count back into the map, either updating an existing entry or creating a new one.

Exercise 4: Merging Two HashMaps

Problem Statement: Create two separate HashMap objects containing employee IDs and their department names. Merge the second map into the first map. If an ID exists in both, append "-Dual" to the department name using Map.merge().

Purpose: This exercise introduces merge(), a method designed specifically for combining maps where the caller decides exactly what should happen when the same key appears in both sources.

Given Input: Map1: {101=Engineering, 102=Sales}, Map2: {102=Marketing, 103=HR}

Expected Output: {101=Engineering, 102=Sales-Dual, 103=HR}

▼ Hint
  • Loop through every entry in the second map.
  • For each entry, call map1.merge(id, department, (oldVal, newVal) -> oldVal + "-Dual").
  • If the key does not yet exist in map1, merge() simply inserts the new value directly, without ever calling the merging function.
▼ Solution & Explanation
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        HashMap<Integer, String> map1 = new HashMap<>();
        map1.put(101, "Engineering");
        map1.put(102, "Sales");

        HashMap<Integer, String> map2 = new HashMap<>();
        map2.put(102, "Marketing");
        map2.put(103, "HR");

        for (Map.Entry<Integer, String> entry : map2.entrySet()) {
            map1.merge(entry.getKey(), entry.getValue(), (oldVal, newVal) -> oldVal + "-Dual");
        }

        System.out.println(map1);
    }
}Code language: Java (java)

Explanation:

  • map1.merge(key, value, function): If key is not already present, it simply inserts value directly, just like put().
  • (oldVal, newVal) -> oldVal + "-Dual": Runs only when the key already exists, receiving the existing value as oldVal and the incoming value as newVal, and returning the combined result to store.
  • 102 becomes "Sales-Dual": Since ID 102 exists in both maps, the merge function appends "-Dual" to the original department name rather than simply overwriting it.

Exercise 5: Default Value with computeIfAbsent()

Problem Statement: Write a program that attempts to look up a stock price in a map. If the stock isn’t listed, use computeIfAbsent() to fetch a default price and add it to the map automatically.

Purpose: This exercise introduces computeIfAbsent(), which combines a lookup and a conditional insert into a single call, useful for lazily populating a map only when a key is first requested.

Given Input: Map: {"AAPL"=190.0, "TSLA"=250.0}, lookup: "GOOG"

Price of GOOG: 100.0
Map after lookup: {AAPL=190.0, TSLA=250.0, GOOG=100.0}
▼ Hint
  • Call stockPrices.computeIfAbsent("GOOG", key -> fetchDefaultPrice(key)).
  • If "GOOG" is already a key, the lambda never runs and the existing value is simply returned.
  • If "GOOG" is missing, the lambda runs, and whatever it returns is both inserted into the map and returned to the caller in the same step.
▼ Solution & Explanation
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Double> stockPrices = new HashMap<>();
        stockPrices.put("AAPL", 190.0);
        stockPrices.put("TSLA", 250.0);

        double price = stockPrices.computeIfAbsent("GOOG", key -> 100.0);

        System.out.println("Price of GOOG: " + price);
        System.out.println("Map after lookup: " + stockPrices);
    }
}Code language: Java (java)

Explanation:

  • stockPrices.computeIfAbsent("GOOG", key -> 100.0): Checks whether "GOOG" is already present. Since it is not, the lambda runs and produces 100.0 as the default price.
  • Automatic insertion: The value returned by the lambda is stored into the map under the key "GOOG" as part of the same call, no separate put() is needed.
  • Existing key behavior: If "AAPL" had been looked up instead, the lambda would never execute, and the stored value 190.0 would simply be returned unchanged.

Exercise 6: hashCode() and equals() in Custom Keys

Problem Statement: Create a custom User class without overriding hashCode() and equals(). Add two identical User objects as keys to a HashMap. Observe the behavior, then fix the class by properly overriding both methods to see how the map’s behavior changes.

Purpose: This exercise demonstrates why HashMap relies on hashCode() and equals() to determine whether two keys are the same, and what goes wrong when a custom class does not define them properly.

Given Input: User u1 = new User("neha_k"); User u2 = new User("neha_k");

Without equals/hashCode, map size: 2
With equals/hashCode, map size: 1
▼ Hint
  • Without overriding anything, User inherits the default hashCode() and equals() from Object, which compare objects by memory reference rather than content.
  • Two separately constructed User objects with the same username will therefore be treated as different keys, even though they look identical.
  • After overriding equals() to compare username fields and hashCode() to be based on the same field, the map will correctly treat the two objects as the same key.
▼ Solution & Explanation
import java.util.HashMap;
import java.util.Objects;

class UserWithoutOverride {
    String username;

    UserWithoutOverride(String username) {
        this.username = username;
    }
}

class UserWithOverride {
    String username;

    UserWithOverride(String username) {
        this.username = username;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof UserWithOverride)) return false;
        UserWithOverride other = (UserWithOverride) obj;
        return username.equals(other.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(username);
    }
}

public class Main {
    public static void main(String[] args) {
        HashMap<UserWithoutOverride, String> brokenMap = new HashMap<>();
        brokenMap.put(new UserWithoutOverride("neha_k"), "Session1");
        brokenMap.put(new UserWithoutOverride("neha_k"), "Session2");
        System.out.println("Without equals/hashCode, map size: " + brokenMap.size());

        HashMap<UserWithOverride, String> fixedMap = new HashMap<>();
        fixedMap.put(new UserWithOverride("neha_k"), "Session1");
        fixedMap.put(new UserWithOverride("neha_k"), "Session2");
        System.out.println("With equals/hashCode, map size: " + fixedMap.size());
    }
}Code language: Java (java)

Explanation:

  • UserWithoutOverride: Relies on the default Object implementation, which treats every new object as a distinct key, so the map ends up with two separate entries despite the identical usernames.
  • public boolean equals(Object obj): Defines what it means for two UserWithOverride objects to be considered equal, comparing their username fields instead of their memory addresses.
  • Objects.hash(username): Produces a hash code based on the same field used in equals(), which is required since HashMap uses hashCode() first to locate the correct bucket before checking equals().
  • Result: The second map correctly recognizes the two UserWithOverride objects as the same key, so the second put() overwrites the first, leaving a size of 1.

Exercise 7: Finding Max and Min Values

Problem Statement: Given a HashMap<String, Integer> representing student names and their exam scores, find and print the key (student) with the highest score and the key with the lowest score without sorting the map.

Purpose: This exercise practices scanning a map’s entries in a single pass to track the running maximum and minimum, avoiding the unnecessary overhead of sorting the entire map just to find two values.

Given Input: Map: {"Amit"=78, "Priya"=92, "Ravi"=65, "Sneha"=88}

Highest scorer: Priya with 92
Lowest scorer: Ravi with 65
▼ Hint
  • Initialize two variables to track the current highest and lowest names, starting from any single entry in the map.
  • Loop through map.entrySet() once, comparing each entry’s score against the current tracked highest and lowest.
  • Update the tracked variables whenever a new highest or lowest score is found during the scan.
▼ Solution & Explanation
import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> scores = new HashMap<>();
        scores.put("Amit", 78);
        scores.put("Priya", 92);
        scores.put("Ravi", 65);
        scores.put("Sneha", 88);

        String highestStudent = null;
        String lowestStudent = null;
        int highestScore = Integer.MIN_VALUE;
        int lowestScore = Integer.MAX_VALUE;

        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            if (entry.getValue() > highestScore) {
                highestScore = entry.getValue();
                highestStudent = entry.getKey();
            }
            if (entry.getValue() < lowestScore) {
                lowestScore = entry.getValue();
                lowestStudent = entry.getKey();
            }
        }

        System.out.println("Highest scorer: " + highestStudent + " with " + highestScore);
        System.out.println("Lowest scorer: " + lowestStudent + " with " + lowestScore);
    }
}Code language: Java (java)

Explanation:

  • Integer.MIN_VALUE and Integer.MAX_VALUE: Used as starting points that any real score will beat, guaranteeing the first comparison always updates the tracked values.
  • if (entry.getValue() > highestScore): Updates the tracked highest student and score only when the current entry beats everything seen so far.
  • Single pass: Both the highest and lowest are found while scanning the entries exactly once, which is more efficient than sorting the entire map first.

Exercise 8: Inverting a Map

Problem Statement: Write a method that takes a HashMap<K, V> and returns a new HashMap<V, List<K>>, effectively inverting the map so that duplicate values aggregate their corresponding keys.

Purpose: This exercise practices building a new map structure from an existing one, and specifically handles the case where the original values are not unique, meaning several keys must be grouped together under one inverted key.

Given Input: Map: {"apple"="fruit", "carrot"="vegetable", "banana"="fruit", "potato"="vegetable"}

Expected Output: {fruit=[apple, banana], vegetable=[carrot, potato]}

▼ Hint
  • Create a new empty HashMap<V, List<K>> to hold the inverted result.
  • Loop through the original map’s entries, and for each one, use computeIfAbsent(value, k -> new ArrayList<>()) to get or create the list associated with that value.
  • Add the original key to that list, so every key that shared the same value ends up grouped together.
▼ Solution & Explanation
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    static <K, V> HashMap<V, List<K>> invertMap(HashMap<K, V> original) {
        HashMap<V, List<K>> inverted = new HashMap<>();
        for (Map.Entry<K, V> entry : original.entrySet()) {
            inverted.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey());
        }
        return inverted;
    }

    public static void main(String[] args) {
        HashMap<String, String> items = new HashMap<>();
        items.put("apple", "fruit");
        items.put("carrot", "vegetable");
        items.put("banana", "fruit");
        items.put("potato", "vegetable");

        HashMap<String, List<String>> invertedItems = invertMap(items);
        System.out.println(invertedItems);
    }
}Code language: Java (java)

Explanation:

  • inverted.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()): Ensures a list exists for the current value, creating an empty one the first time that value is encountered.
  • .add(entry.getKey()): Appends the original key to the list associated with its value, so entries like "apple" and "banana" both end up under "fruit".
  • Generic method: Declaring the method as static <K, V> lets it work with a map of any key and value types, not just String pairs.

Exercise 9: TreeMap Natural Ordering

Problem Statement: Create a TreeMap of product IDs (Strings) and prices (Doubles). Insert elements out of order and print the map to demonstrate how it automatically sorts keys alphabetically.

Purpose: This exercise introduces TreeMap, which keeps its keys in sorted order at all times, unlike HashMap, which makes no guarantee about iteration order.

Given Input: Insert order: "P300", "P100", "P200"

Expected Output: {P100=45.0, P200=30.0, P300=99.5}

▼ Hint
  • Insert the three product entries in the exact order shown, without worrying about their final position.
  • Simply print the TreeMap directly.
  • Unlike a HashMap, a TreeMap always iterates its keys in their natural sorted order, which for String keys means alphabetical order.
▼ Solution & Explanation
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<String, Double> productPrices = new TreeMap<>();
        productPrices.put("P300", 99.5);
        productPrices.put("P100", 45.0);
        productPrices.put("P200", 30.0);

        System.out.println(productPrices);
    }
}Code language: Java (java)

Explanation:

  • productPrices.put("P300", 99.5): Insertion order does not matter to a TreeMap, so entries can be added in any sequence.
  • Internal structure: A TreeMap stores its entries in a sorted tree structure rather than the bucket-based structure a HashMap uses, which is what keeps the keys ordered automatically.
  • Result: Printing the map shows the keys in alphabetical order, P100, P200, then P300, regardless of the order they were inserted.

Exercise 10: TreeMap with a Custom Comparator

Problem Statement: Create a TreeMap that stores employee names and salaries, but configure it via a custom Comparator to sort the names in reverse alphabetical order.

Purpose: This exercise practices supplying a custom Comparator to a TreeMap‘s constructor, showing how the natural sort order can be overridden entirely with your own comparison rule.

Given Input: Insert order: "Aman", "Divya", "Karan"

Expected Output: {Karan=55000.0, Divya=62000.0, Aman=48000.0}

▼ Hint
  • Pass a Comparator<String> into the TreeMap constructor instead of leaving it empty.
  • Use Comparator.reverseOrder(), or a lambda like (a, b) -> b.compareTo(a), to reverse the default alphabetical comparison.
  • Every subsequent put() call will be positioned according to this custom ordering rather than natural ordering.
▼ Solution & Explanation
import java.util.Comparator;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<String, Double> salaries = new TreeMap<>(Comparator.reverseOrder());
        salaries.put("Aman", 48000.0);
        salaries.put("Divya", 62000.0);
        salaries.put("Karan", 55000.0);

        System.out.println(salaries);
    }
}Code language: Java (java)

Explanation:

  • new TreeMap<>(Comparator.reverseOrder()): Passes a custom Comparator into the constructor, telling the TreeMap to use reverse alphabetical order instead of its default natural ordering.
  • Comparator.reverseOrder(): A built-in comparator that reverses whatever the natural ordering of the type would otherwise be, which for String means Z before A.
  • Result: Regardless of the order the three names were inserted, printing the map shows Karan, Divya, then Aman, following the reversed alphabetical rule.

Exercise 11: First and Last Entry in a TreeMap

Problem Statement: Given a TreeMap of timestamps (Long) and system logs (String), write code to find the exact first and last entries in the map using firstEntry() and lastEntry().

Purpose: This exercise introduces the direct entry-retrieval methods that TreeMap offers thanks to its sorted structure, giving instant access to the smallest and largest keyed entries without scanning the whole map.

Given Input: Map: {1000=Server started, 3000=User login, 2000=Cache cleared, 5000=Server stopped}

First entry: 1000=Server started
Last entry: 5000=Server stopped
▼ Hint
  • Call logs.firstEntry(), which returns the entry with the smallest key as a Map.Entry.
  • Call logs.lastEntry(), which returns the entry with the largest key.
  • Both methods return null if the map is empty, so no manual scanning or sorting is needed.
▼ Solution & Explanation
import java.util.Map;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<Long, String> logs = new TreeMap<>();
        logs.put(1000L, "Server started");
        logs.put(3000L, "User login");
        logs.put(2000L, "Cache cleared");
        logs.put(5000L, "Server stopped");

        Map.Entry<Long, String> first = logs.firstEntry();
        Map.Entry<Long, String> last = logs.lastEntry();

        System.out.println("First entry: " + first.getKey() + "=" + first.getValue());
        System.out.println("Last entry: " + last.getKey() + "=" + last.getValue());
    }
}Code language: Java (java)

Explanation:

  • logs.firstEntry(): Because the underlying tree keeps keys sorted, the smallest key is always the leftmost node, so this method can return it directly without a search.
  • logs.lastEntry(): Similarly returns the rightmost node in the tree, which holds the largest key.
  • Insertion order irrelevant: Even though 3000L was inserted before 2000L, the sorted structure of TreeMap ensures firstEntry() and lastEntry() always reflect the true minimum and maximum keys.

Exercise 12: floorKey() and ceilingKey()

Problem Statement: You have a TreeMap<Integer, String> representing coupon codes and their required minimum purchase amounts. Use floorKey() and ceilingKey() to find the best available coupon for a user spending exactly $75.

Purpose: This exercise introduces floorKey() and ceilingKey(), which locate the closest key at or below, and at or above, a given value, a common need in tiered pricing and threshold based logic.

Given Input: Map: {20="WELCOME5", 50="SAVE10", 75="SAVE15", 100="SAVE20", 150="SAVE30"}, spend = 75

Floor key for 75: 75
Ceiling key for 75: 75
▼ Hint
  • Call coupons.floorKey(75), which returns the largest key that is less than or equal to 75.
  • Call coupons.ceilingKey(75), which returns the smallest key that is greater than or equal to 75.
  • Since 75 itself is an exact key in the map, both methods return 75 directly, matching the coupon exactly.
▼ Solution & Explanation
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<Integer, String> coupons = new TreeMap<>();
        coupons.put(20, "WELCOME5");
        coupons.put(50, "SAVE10");
        coupons.put(75, "SAVE15");
        coupons.put(100, "SAVE20");
        coupons.put(150, "SAVE30");

        int spend = 75;

        Integer floor = coupons.floorKey(spend);
        Integer ceiling = coupons.ceilingKey(spend);

        System.out.println("Floor key for " + spend + ": " + floor);
        System.out.println("Ceiling key for " + spend + ": " + ceiling);
    }
}Code language: Java (java)

Explanation:

  • coupons.floorKey(75): Searches for the largest key not exceeding 75. Since 75 is itself present as a key, it is returned exactly.
  • coupons.ceilingKey(75): Searches for the smallest key not smaller than 75, which is also 75 in this case.
  • When there is no exact match: If the user had spent 80 instead, floorKey(80) would return 75 and ceilingKey(80) would return 100, showing how the two methods diverge once the key falls between two actual entries.

Exercise 13: lowerKey() and higherKey()

Problem Statement: Using the same coupon map from the previous exercise, use lowerKey() and higherKey() to find coupons strictly below or strictly above a $75 threshold.

Purpose: This exercise contrasts lowerKey() and higherKey() with floorKey() and ceilingKey(), highlighting that these variants exclude an exact match, which matters whenever the boundary value itself must not count.

Given Input: Map: {20="WELCOME5", 50="SAVE10", 75="SAVE15", 100="SAVE20", 150="SAVE30"}, threshold = 75

Lower key than 75: 50
Higher key than 75: 100
▼ Hint
  • Call coupons.lowerKey(75), which finds the largest key strictly less than 75, skipping over 75 itself even though it exists in the map.
  • Call coupons.higherKey(75), which finds the smallest key strictly greater than 75.
  • Compare these results to floorKey(75) and ceilingKey(75) from the previous exercise, both of which returned 75 directly.
▼ Solution & Explanation
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<Integer, String> coupons = new TreeMap<>();
        coupons.put(20, "WELCOME5");
        coupons.put(50, "SAVE10");
        coupons.put(75, "SAVE15");
        coupons.put(100, "SAVE20");
        coupons.put(150, "SAVE30");

        int threshold = 75;

        Integer lower = coupons.lowerKey(threshold);
        Integer higher = coupons.higherKey(threshold);

        System.out.println("Lower key than " + threshold + ": " + lower);
        System.out.println("Higher key than " + threshold + ": " + higher);
    }
}Code language: Java (java)

Explanation:

  • coupons.lowerKey(75): Even though 75 exists in the map, this method deliberately excludes it and returns 50, the next key down.
  • coupons.higherKey(75): Skips past 75 as well, returning 100, the next key up.
  • Key distinction: The lower and higher family of methods never return the exact key you pass in, whereas floor and ceiling will if that key is actually present.

Exercise 14: Extracting a Range with subMap()

Problem Statement: Create a TreeMap representing a schedule (Key: hour as an Integer from 0 to 23, Value: task name). Use subMap() to extract and print all tasks scheduled between 9 AM (inclusive) and 5 PM (exclusive).

Purpose: This exercise practices extracting a contiguous range of entries from a sorted map using subMap(), which is far more efficient than manually filtering every entry with an if check.

Given Input: Map: {8="Breakfast", 9="Standup", 12="Lunch", 14="Client Call", 17="Wrap-up", 20="Dinner"}

{9=Standup, 12=Lunch, 14=Client Call}
▼ Hint
  • Call schedule.subMap(9, 17), which returns a view containing all entries with keys from 9 up to but not including 17.
  • This matches the requirement of 9 AM inclusive and 5 PM exclusive exactly, since 17 represents 5 PM in 24 hour format.
  • The returned subMap() is a live view backed by the original map, so changes to it would also affect the original.
▼ Solution & Explanation
import java.util.SortedMap;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<Integer, String> schedule = new TreeMap<>();
        schedule.put(8, "Breakfast");
        schedule.put(9, "Standup");
        schedule.put(12, "Lunch");
        schedule.put(14, "Client Call");
        schedule.put(17, "Wrap-up");
        schedule.put(20, "Dinner");

        SortedMap<Integer, String> workingHours = schedule.subMap(9, 17);

        System.out.println(workingHours);
    }
}Code language: Java (java)

Explanation:

  • schedule.subMap(9, 17): Extracts every entry whose key falls in the range starting at 9 and ending just before 17, taking advantage of the sorted structure to do this efficiently.
  • Inclusive start, exclusive end: The two-argument version of subMap() always includes the start key and excludes the end key, which is why 8 and 17 are absent from the result but 9 is present.
  • SortedMap<Integer, String>: subMap() returns a SortedMap view rather than a fresh independent copy, which is why the return type is declared as the more general interface.

Exercise 15: Splitting a Map with headMap() and tailMap()

Problem Statement: Using a TreeMap of inventory items and stock levels, use headMap() to get all items with IDs lexicographically before a certain point, and tailMap() to get everything after.

Purpose: This exercise practices splitting a sorted map into two complementary halves using headMap() and tailMap(), both of which rely on the map’s natural key ordering to define the split point.

Given Input: Map: {"A100"=50, "B200"=30, "C150"=80, "D300"=20}

Head (before C150): {A100=50, B200=30}
Tail (from C150): {C150=80, D300=20}
▼ Hint
  • Call inventory.headMap("C150"), which returns every entry with a key strictly less than "C150", in lexicographical order.
  • Call inventory.tailMap("C150"), which returns every entry with a key greater than or equal to "C150".
  • Together, headMap() and tailMap() at the same split point always account for every entry in the original map exactly once.
▼ Solution & Explanation
import java.util.SortedMap;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<String, Integer> inventory = new TreeMap<>();
        inventory.put("A100", 50);
        inventory.put("B200", 30);
        inventory.put("C150", 80);
        inventory.put("D300", 20);

        SortedMap<String, Integer> head = inventory.headMap("C150");
        SortedMap<String, Integer> tail = inventory.tailMap("C150");

        System.out.println("Head (before C150): " + head);
        System.out.println("Tail (from C150): " + tail);
    }
}Code language: Java (java)

Explanation:

  • inventory.headMap("C150"): Returns everything lexicographically before "C150", which excludes "C150" itself.
  • inventory.tailMap("C150"): Returns everything from "C150" onward, including "C150" itself, which is why it appears in the tail rather than the head.
  • Complementary split: Every original entry appears in exactly one of the two resulting maps, with no overlap and no entry left out.

Exercise 16: Reversing a TreeMap with descendingMap()

Problem Statement: Take an existing, populated TreeMap and create a reverse-order view of it using the descendingMap() method.

Purpose: This exercise introduces descendingMap(), which flips the iteration order of a TreeMap entirely, useful whenever you need the largest keys first without rebuilding the map manually.

Given Input: Map: {1=One, 2=Two, 3=Three, 4=Four}

Expected Output: {4=Four, 3=Three, 2=Two, 1=One}

▼ Hint
  • Call numbers.descendingMap(), which returns a NavigableMap view of the same entries but in reverse key order.
  • Print the returned map directly to see the entries appear from highest key to lowest.
  • This view is backed by the original map, so it always reflects the current state of numbers, it does not create a separate copy.
▼ Solution & Explanation
import java.util.NavigableMap;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<Integer, String> numbers = new TreeMap<>();
        numbers.put(1, "One");
        numbers.put(2, "Two");
        numbers.put(3, "Three");
        numbers.put(4, "Four");

        NavigableMap<Integer, String> reversed = numbers.descendingMap();

        System.out.println(reversed);
    }
}Code language: Java (java)

Explanation:

  • numbers.descendingMap(): Produces a view where iterating the entries visits them from the highest key down to the lowest, the exact opposite of the map’s normal order.
  • NavigableMap<Integer, String>: The return type supports the same range and navigation methods as TreeMap itself, just operating in the reversed direction.
  • Live view: Because reversed is backed by numbers, any future changes made to numbers would automatically be reflected when reversed is printed again.

Exercise 17: HashMap vs LinkedHashMap Order

Problem Statement: Insert 5 random pairs into a HashMap and the exact same 5 pairs into a LinkedHashMap. Print both maps to visually contrast the unpredictable order of HashMap against the strict insertion order of LinkedHashMap.

Purpose: This exercise puts HashMap and LinkedHashMap side by side using identical data, making the ordering guarantee that LinkedHashMap provides, and that HashMap does not, directly visible.

Given Input: Insert order: "Zebra", "Apple", "Mango", "Banana", "Kiwi"

HashMap order (unpredictable): {Banana=4, Apple=2, Kiwi=5, Zebra=1, Mango=3}
LinkedHashMap order (insertion): {Zebra=1, Apple=2, Mango=3, Banana=4, Kiwi=5}
▼ Hint
  • Insert the exact same five key-value pairs, in the exact same order, into both a HashMap and a LinkedHashMap.
  • Print each map directly afterward.
  • The HashMap‘s printed order depends on each key’s hash code and internal bucket placement, and is not guaranteed to match insertion order, while the LinkedHashMap always preserves it.
▼ Solution & Explanation
import java.util.HashMap;
import java.util.LinkedHashMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> hashMap = new HashMap<>();
        LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();

        String[] keys = {"Zebra", "Apple", "Mango", "Banana", "Kiwi"};
        for (int i = 0; i < keys.length; i++) {
            hashMap.put(keys[i], i + 1);
            linkedHashMap.put(keys[i], i + 1);
        }

        System.out.println("HashMap order (unpredictable): " + hashMap);
        System.out.println("LinkedHashMap order (insertion): " + linkedHashMap);
    }
}Code language: Java (java)

Explanation:

  • HashMap ordering: Entries are placed into internal buckets based on each key’s hash code, so the printed order can differ from insertion order and may even change between different Java versions or runs.
  • LinkedHashMap ordering: Internally maintains a doubly linked list connecting the entries in the order they were inserted, so iterating or printing it always reproduces that exact insertion sequence.
  • Same data, different guarantees: Both maps hold identical key-value pairs, but only one of them makes any promise about the order those pairs will be visited in.

Exercise 18: LinkedHashMap Access Order

Problem Statement: Initialize a LinkedHashMap with its access-order flag set to true. Insert 4 entries, access the second entry twice, and print the map to see how the accessed entry automatically jumps to the end of the iteration order.

Purpose: This exercise introduces the access-order mode of LinkedHashMap, which reorders entries based on when they were last read rather than when they were inserted, forming the basis of simple LRU cache designs.

Given Input: Insert order: "A", "B", "C", "D"; then get("B") is called twice

Expected Output: {A=1, C=3, D=4, B=2}

▼ Hint
  • Use the three-argument LinkedHashMap constructor, new LinkedHashMap<>(initialCapacity, loadFactor, true), where the final true enables access order.
  • Insert the four entries in order, then call map.get("B") twice.
  • In access-order mode, every successful get() call moves that entry to the end of the iteration order, so "B" ends up last even though it was originally inserted second.
▼ Solution & Explanation
import java.util.LinkedHashMap;

public class Main {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);
        map.put("D", 4);

        map.get("B");
        map.get("B");

        System.out.println(map);
    }
}Code language: Java (java)

Explanation:

  • new LinkedHashMap<>(16, 0.75f, true): The final true switches the map from its default insertion-order mode into access-order mode.
  • map.get("B"): Every time this runs successfully, the entry for "B" is moved to the end of the internal linked list, marking it as the most recently used.
  • Result: Even though "B" was the second entry inserted, calling get("B") twice pushes it to the very end of the printed order, ahead of nothing else since it is now the most recently accessed.

Exercise 19: Building an LRU Cache

Problem Statement: Extend LinkedHashMap to create a simple LRU (Least Recently Used) Cache with a maximum capacity of 3 items. Override removeEldestEntry() so that the oldest, least-accessed item is automatically evicted when a 4th item is added.

Purpose: This exercise combines access-order mode with removeEldestEntry() to build a genuine fixed-size cache, showing how LinkedHashMap was specifically designed to support this exact pattern.

Given Input: Insert order: put(1,"A"), put(2,"B"), put(3,"C"), get(1), put(4,"D")

Expected Output: {3=C, 1=A, 4=D}

▼ Hint
  • Create a class that extends LinkedHashMap<K, V>, calling super(capacity, 0.75f, true) in its constructor to enable access order.
  • Override removeEldestEntry(Map.Entry<K, V> eldest) to return true whenever size() > capacity.
  • This method is automatically called by LinkedHashMap internally right after every put(), and returning true tells it to remove the eldest entry immediately.
▼ Solution & Explanation
import java.util.LinkedHashMap;
import java.util.Map;

class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

public class Main {
    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);

        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        cache.get(1);
        cache.put(4, "D");

        System.out.println(cache);
    }
}Code language: Java (java)

Explanation:

  • super(capacity, 0.75f, true): Enables access order in the parent LinkedHashMap, which is required for the cache to correctly identify the least recently used entry.
  • removeEldestEntry(Map.Entry<K, V> eldest): Runs automatically after every insertion, receiving the entry that would be evicted if this method returns true.
  • cache.get(1): Marks key 1 as recently used, moving it away from being the eldest entry, which is why key 2 is evicted instead when put(4, "D") pushes the cache over capacity.

Exercise 20: First and Last Key in LinkedHashMap

Problem Statement: Write a method for a standard insertion-order LinkedHashMap that retrieves the very first (oldest inserted) element and the very last (most recently inserted) element without looping through the entire collection.

Purpose: This exercise practices reaching directly for the boundary elements of an ordered map using its iterator and array conversion, instead of writing a manual scan that tracks the first and last values seen.

Given Input: Insert order: "First", "Second", "Third", "Fourth"

Oldest key: First
Newest key: Fourth
▼ Hint
  • Get the oldest key using map.keySet().iterator().next(), which grabs the very first key without visiting any of the others.
  • Convert the key set to an array with map.keySet().toArray(), then read the newest key using the last index, array[array.length - 1].
  • Because LinkedHashMap preserves insertion order, the first key produced by its iterator is always the oldest entry, and the last position in the array is always the newest.
▼ Solution & Explanation
import java.util.LinkedHashMap;

public class Main {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
        map.put("First", 1);
        map.put("Second", 2);
        map.put("Third", 3);
        map.put("Fourth", 4);

        String oldestKey = map.keySet().iterator().next();

        Object[] keys = map.keySet().toArray();
        Object newestKey = keys[keys.length - 1];

        System.out.println("Oldest key: " + oldestKey);
        System.out.println("Newest key: " + newestKey);
    }
}Code language: Java (java)

Explanation:

  • map.keySet().iterator().next(): Requests only a single element from the iterator, stopping immediately after the very first key rather than continuing through the rest.
  • map.keySet().toArray(): Copies the keys into an array in their current insertion order, letting the last element be reached directly by index.
  • keys[keys.length - 1]: Since the array mirrors the map’s insertion order, this index always points at the most recently inserted key.

Exercise 21: Updating a Value Without Changing Order

Problem Statement: Given a LinkedHashMap, update the value of an existing key. Verify whether updating the value alters the element’s original position in the insertion order.

Purpose: This exercise clarifies a subtle but important distinction in a standard insertion-order LinkedHashMap, that overwriting a value through put() does not move the entry, unlike the access-order mode explored in earlier exercises.

Given Input: Map: {"A"=1, "B"=2, "C"=3}, update: put("A", 100)

Expected Output: {A=100, B=2, C=3}

▼ Hint
  • Create a regular LinkedHashMap without passing the access-order flag, so it defaults to insertion order.
  • Call put("A", 100) again on the existing key "A".
  • Print the map afterward and check whether "A" is still first, confirming that a value update alone does not affect ordering in the default mode.
▼ Solution & Explanation
import java.util.LinkedHashMap;

public class Main {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);

        map.put("A", 100);

        System.out.println(map);
    }
}Code language: Java (java)

Explanation:

  • new LinkedHashMap<>(): Without the three-argument constructor used in earlier access-order exercises, this map defaults to preserving insertion order only.
  • map.put("A", 100): Since "A" already exists, this overwrites its value in place without removing and reinserting the entry.
  • Result: "A" remains the first entry in the printed map, confirming that a value update by itself never changes an entry’s position in insertion-order mode.

Exercise 22: Grouping Objects with Streams

Problem Statement: Given a List of Product objects (each having a category, name, and price), use Java Streams to group them into a Map<String, List<Product>> based on their category.

Purpose: This exercise practices Collectors.groupingBy(), a Stream API method that builds a grouped map directly from a list in a single expression, replacing what would otherwise require a manual loop and computeIfAbsent().

Given Input: Products: ("Laptop", "Electronics", 999), ("Shirt", "Clothing", 29), ("Phone", "Electronics", 599), ("Jeans", "Clothing", 49)

Electronics: [Laptop, Phone]
Clothing: [Shirt, Jeans]
▼ Hint
  • Give the Product class fields for name, category, and price, along with a getter for each.
  • Call products.stream().collect(Collectors.groupingBy(Product::getCategory)) to build the grouped map in one line.
  • Each key in the resulting map is a distinct category, and each value is a List<Product> containing every product that shares that category.
▼ Solution & Explanation
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

class Product {
    private String name;
    private String category;
    private double price;

    public Product(String name, String category, double price) {
        this.name = name;
        this.category = category;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public String getCategory() {
        return category;
    }

    @Override
    public String toString() {
        return name;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
            new Product("Laptop", "Electronics", 999),
            new Product("Shirt", "Clothing", 29),
            new Product("Phone", "Electronics", 599),
            new Product("Jeans", "Clothing", 49)
        );

        Map<String, List<Product>> byCategory = products.stream()
            .collect(Collectors.groupingBy(Product::getCategory));

        for (Map.Entry<String, List<Product>> entry : byCategory.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}Code language: Java (java)

Explanation:

  • products.stream(): Converts the List<Product> into a stream so it can be processed with the Stream API.
  • Collectors.groupingBy(Product::getCategory): Groups every product by the result of calling getCategory() on it, automatically creating a new list for each distinct category encountered.
  • Product::getCategory: A method reference that tells the collector which field to group by, equivalent to writing p -> p.getCategory().

Exercise 23: Converting HashMap to TreeMap

Problem Statement: Start with a chaotic, unsorted HashMap. Write a single line of code (or a brief method) to convert it into a TreeMap so that all existing elements become instantly sorted by key.

Purpose: This exercise practices converting between map implementations using a constructor call, showing that any Map can be handed directly to a TreeMap constructor to produce a sorted copy in one step.

Given Input: HashMap: {"Zebra"=1, "Apple"=2, "Mango"=3}

Expected Output: {Apple=2, Mango=3, Zebra=1}

▼ Hint

Pass the existing HashMap directly into the TreeMap constructor, new TreeMap<>(unsortedMap), which copies every entry and immediately arranges them according to natural key order.

▼ Solution & Explanation
import java.util.HashMap;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> unsortedMap = new HashMap<>();
        unsortedMap.put("Zebra", 1);
        unsortedMap.put("Apple", 2);
        unsortedMap.put("Mango", 3);

        TreeMap<String, Integer> sortedMap = new TreeMap<>(unsortedMap);

        System.out.println(sortedMap);
    }
}Code language: Java (java)

Explanation:

  • new TreeMap<>(unsortedMap): TreeMap has a constructor that accepts any Map, copying its entries in and sorting them according to their natural ordering as part of construction.
  • Independent copy: sortedMap is a completely separate object, so further changes to unsortedMap afterward would not affect the sorted copy.
  • Result: Printing sortedMap shows the same three entries as the original HashMap, but now arranged alphabetically by key.

Exercise 24: Sorting a Map by Value

Problem Statement: Because maps sort by keys by default, write a program that takes a HashMap<String, Integer> and sorts it based entirely on its values in ascending order, returning a new LinkedHashMap to preserve that sorted state.

Purpose: This exercise practices sorting map entries by value using the Stream API, then rebuilding the result into a LinkedHashMap, the only common map type capable of preserving a specific, manually determined order.

Given Input: Map: {"Charlie"=85, "Alice"=92, "Bob"=70}

Expected Output: {Bob=70, Charlie=85, Alice=92}

▼ Hint
  • Call map.entrySet().stream() to begin working with the entries as a stream.
  • Use .sorted(Map.Entry.comparingByValue()) to sort the stream by each entry’s value instead of its key.
  • Collect the sorted stream into a LinkedHashMap using Collectors.toMap() with a merge function and LinkedHashMap::new as the map supplier, since only a LinkedHashMap will remember the order the entries were inserted in.
▼ Solution & Explanation
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> scores = new HashMap<>();
        scores.put("Charlie", 85);
        scores.put("Alice", 92);
        scores.put("Bob", 70);

        LinkedHashMap<String, Integer> sortedByValue = scores.entrySet().stream()
            .sorted(Map.Entry.comparingByValue())
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                Map.Entry::getValue,
                (a, b) -> a,
                LinkedHashMap::new
            ));

        System.out.println(sortedByValue);
    }
}Code language: Java (java)

Explanation:

  • Map.Entry.comparingByValue(): A ready-made comparator that orders Map.Entry objects by their value in ascending order, avoiding the need to write a custom comparator by hand.
  • Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new): Rebuilds a map from the sorted stream, pulling the key and value from each entry, using (a, b) -> a to resolve any duplicate keys, and specifically constructing a LinkedHashMap to preserve the sorted stream order.
  • Why LinkedHashMap: A plain HashMap would immediately scramble the carefully sorted order again, since it has no concept of insertion order, which is why the map supplier matters here.

Exercise 25: Thread Safety: HashMap vs ConcurrentHashMap

Problem Statement: Create a regular HashMap and attempt to update it simultaneously from three different threads. Observe the potential for a ConcurrentModificationException or data loss. Then, resolve the issue using Collections.synchronizedMap() or discuss how ConcurrentHashMap solves this more efficiently.

Purpose: This exercise demonstrates why HashMap is unsafe under concurrent writes, then compares two standard fixes, wrapping a map with a single lock versus using a map designed from the ground up for concurrent access.

Given Input: Three threads each inserting 1000 unique key-value pairs into the same map at the same time.

Unsafe HashMap final size: 1000 (unreliable, may vary or throw an exception between runs)
Synchronized HashMap final size: 1000 (reliable)
ConcurrentHashMap final size: 1000 (reliable)
▼ Hint
  • Create a plain HashMap and start three Thread objects that all call put() on it at the same time, then call join() on all three before checking the final size.
  • Because HashMap performs no internal locking, concurrent writes can corrupt its internal bucket structure, occasionally causing lost entries, incorrect sizes, or even an infinite loop during a resize.
  • Wrap a HashMap with Collections.synchronizedMap(new HashMap<>()) to make every operation acquire the same single lock before proceeding, which is safe but forces all threads to wait on each other.
  • Use a ConcurrentHashMap instead for a thread-safe map that allows multiple threads to write to different parts of the map at the same time, rather than blocking on one single lock.
▼ Solution & Explanation
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Map<Integer, Integer> unsafeMap = new HashMap<>();
        runConcurrentInsert(unsafeMap);
        System.out.println("Unsafe HashMap final size: " + unsafeMap.size());

        Map<Integer, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
        runConcurrentInsert(synchronizedMap);
        System.out.println("Synchronized HashMap final size: " + synchronizedMap.size());

        Map<Integer, Integer> concurrentMap = new ConcurrentHashMap<>();
        runConcurrentInsert(concurrentMap);
        System.out.println("ConcurrentHashMap final size: " + concurrentMap.size());
    }

    static void runConcurrentInsert(Map<Integer, Integer> map) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                map.put(i, i);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        Thread t3 = new Thread(task);

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();
    }
}Code language: Java (java)

Explanation:

  • Unsafe HashMap: All three threads write to the same underlying bucket array with no coordination, so a resize operation triggered by one thread can corrupt the structure another thread is simultaneously reading or writing, occasionally producing a wrong size, lost entries, or in rare cases an infinite loop.
  • Collections.synchronizedMap(new HashMap<>()): Wraps the map so every method call acquires a single shared lock first, guaranteeing correctness, but forcing every thread to wait its turn even when writing to completely unrelated keys.
  • ConcurrentHashMap: Achieves thread safety without a single global lock, internally dividing its structure so that operations on different parts of the map can proceed simultaneously, which is why it is generally recommended over synchronizedMap() for concurrent workloads.
  • t1.join(); t2.join(); t3.join();: Ensures the main thread waits for all three worker threads to fully finish inserting before the final size is printed, so the result reflects the completed work rather than a partial state.

Filed Under: Java Exercises

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

Java Exercises

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises
Java Exercises
C# Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Leave a Reply Cancel reply

your email address will NOT be published. all comments are moderated according to our comment policy.

Use <pre> tag for posting code. E.g. <pre> Your entire code </pre>

In: Java Exercises
TweetF  sharein  shareP  Pin

  Java Exercises

  • All Java Exercises
  • Java Exercise for Beginners
  • Java Loops Exercise
  • Java String Exercise
  • Java ArrayList Exercise
  • Java LinkedList Exercise
  • Java HashMap and TreeMap Exercise
  • Java HashSet and TreeSet Exercise
  • Java OOP Exercise
  • Java Methods Exercise
  • Java Enums Exercise
  • Java Exception Handling Exercise
  • Java File Handling Exercise
  • Java Date and Time Exercise
  • Java Data Structures Exercise
  • Java Sorting and Searching Exercise
  • Java Lambda and Functional Interfaces Exercise
  • Java Regex Exercise
  • Java Random Data Generation Exercise
  • Java Generics Exercise
  • Java Reflection Exercise
  • Java JDBC Exercise

All Coding Exercises

Python Exercises C Exercises C++ Exercises Java Exercises C# Exercises

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Coding Exercises

  • C Exercises
  • C++ Exercises
  • Python Exercises
  • Java Exercises
  • C# Exercises

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2026 pynative.com