Skip to main content

11.4 Map: HashMap, LinkedHashMap, and TreeMap

Overview

Map is a collection that stores data as key-value pairs. Think of it like a real dictionary: you look up a word (key) and find its definition (value). Maps provide instant value lookup by key, making them ideal for caching, counting frequencies, grouping data, and configuration storage.

Key characteristics:

  • Keys must be unique— adding the same key replaces the existing value
  • Values may be duplicated
  • Map does not extend Collection, but is part of the collection framework

1. HashMap

HashMap is the most commonly used Map. It uses hashing to provide O(1) average performance for get and put. The iteration 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", 87);
scores.put("Carol", 92);

// Updating (same key → replaces value)
scores.put("Alice", 98); // Alice's score updated

System.out.println(scores); // {Alice=98, Bob=87, Carol=92} (order varies)
System.out.println(scores.size()); // 3

// Retrieving
System.out.println(scores.get("Bob")); // 87
System.out.println(scores.get("Unknown")); // null (key not found)

// Checking existence
System.out.println(scores.containsKey("Carol")); // true
System.out.println(scores.containsValue(87)); // true

// Removing
scores.remove("Bob");
System.out.println(scores); // {Alice=98, Carol=92}

// Iterating
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}

// Keys and values separately
for (String key : scores.keySet()) System.out.println(key);
for (int value : scores.values()) System.out.println(value);

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

2. LinkedHashMap

LinkedHashMap preserves insertion order(or access order if configured). Same O(1) performance as HashMap, but iteration follows the order entries were added.

import java.util.LinkedHashMap;
import java.util.Map;

Map<String, Integer> ordered = new LinkedHashMap<>();
ordered.put("banana", 3);
ordered.put("apple", 5);
ordered.put("cherry", 2);

System.out.println(ordered); // {banana=3, apple=5, cherry=2} — insertion order!

// Access-order LinkedHashMap (LRU cache base)
Map<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return size() > 3; // keep only 3 entries (LRU cache)
}
};

lru.put("a", 1);
lru.put("b", 2);
lru.put("c", 3);
lru.get("a"); // access "a" — moves it to end
lru.put("d", 4); // adds "d", "b" is evicted (least recently used)
System.out.println(lru); // {c=3, a=1, d=4}

3. TreeMap

TreeMap stores entries sorted by key(natural order or custom Comparator). Implemented as a Red-Black tree — all operations are O(log n). Implements NavigableMap for range queries.

import java.util.TreeMap;
import java.util.NavigableMap;

TreeMap<String, Integer> tree = new TreeMap<>();
tree.put("cherry", 5);
tree.put("apple", 3);
tree.put("banana", 7);
tree.put("date", 2);

System.out.println(tree); // {apple=3, banana=7, cherry=5, date=2} — always sorted by key!

// NavigableMap methods
System.out.println(tree.firstKey()); // apple
System.out.println(tree.lastKey()); // date

System.out.println(tree.floorKey("blueberry")); // banana (largest key <= "blueberry")
System.out.println(tree.ceilingKey("blueberry")); // cherry (smallest key >= "blueberry")
System.out.println(tree.lowerKey("cherry")); // banana (strictly < "cherry")
System.out.println(tree.higherKey("cherry")); // date (strictly > "cherry")

// Range view
System.out.println(tree.subMap("apple", true, "cherry", true)); // {apple=3, banana=7, cherry=5}
System.out.println(tree.headMap("cherry")); // {apple=3, banana=7} (keys < "cherry")
System.out.println(tree.tailMap("banana")); // {banana=7, cherry=5, date=2} (keys >= "banana")

4. Useful Map Methods (Java 8+)

Modern Java added many powerful methods to Map:

import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;

Map<String, Integer> stock = new HashMap<>();
stock.put("apple", 10);
stock.put("banana", 5);

// getOrDefault: returns default if key not found (avoids null check)
int appleCount = stock.getOrDefault("apple", 0); // 10
int grapeCount = stock.getOrDefault("grape", 0); // 0 (not null!)
System.out.println("Apple: " + appleCount + ", Grape: " + grapeCount);

// putIfAbsent: only adds if key does not exist
stock.putIfAbsent("apple", 999); // ignored — apple already exists
stock.putIfAbsent("cherry", 8); // added — cherry is new
System.out.println(stock.get("apple")); // 10 (unchanged)
System.out.println(stock.get("cherry")); // 8

// computeIfAbsent: compute and store value if key is absent
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("apple");
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("banana"); // key exists, no recompute
groups.computeIfAbsent("vegs", k -> new ArrayList<>()).add("carrot");
System.out.println(groups); // {fruits=[apple, banana], vegs=[carrot]}

// computeIfPresent: update only if key exists
stock.computeIfPresent("apple", (key, val) -> val + 5); // apple: 10 + 5 = 15
stock.computeIfPresent("grape", (key, val) -> val + 5); // no-op (grape absent)
System.out.println(stock.get("apple")); // 15

// compute: always compute (present or absent)
stock.compute("apple", (key, val) -> val == null ? 1 : val + 1);

// merge: merge a new value with existing one
Map<String, Integer> wordCount = new HashMap<>();
String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
for (String word : words) {
wordCount.merge(word, 1, Integer::sum); // if absent: put 1, else sum
}
System.out.println(wordCount); // {apple=3, banana=2, cherry=1}

// replaceAll: transform all values
stock.replaceAll((key, val) -> val * 2); // double all stock quantities

5. Choosing the Right Map

FeatureHashMapLinkedHashMapTreeMap
OrderNoneInsertion (or access) orderSorted by key
Performance (get/put)O(1) averageO(1) averageO(log n)
Null keysYes (one null)Yes (one null)No (throws NPE)
Range queriesNoNoYes
Best forFast lookupOrdered maps, LRU cacheSorted iteration, range queries

6. Practical Example: Word Frequency Counter

import java.util.*;
import java.util.stream.*;

public class WordFrequencyCounter {

public static Map<String, Long> countWords(String text) {
return Arrays.stream(text.toLowerCase()
.replaceAll("[^a-z\\s]", "") // remove punctuation
.split("\\s+"))
.filter(w -> !w.isEmpty())
.collect(Collectors.groupingBy(
w -> w,
LinkedHashMap::new, // preserve insertion order
Collectors.counting()
));
}

public static List<Map.Entry<String, Long>> topN(Map<String, Long> freq, int n) {
return freq.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.limit(n)
.collect(Collectors.toList());
}

public static void main(String[] args) {
String text = "Java is great. Java is powerful. Spring is built on Java. " +
"Java is used for web, mobile, and enterprise applications.";

Map<String, Long> freq = countWords(text);

System.out.println("Top 5 words:");
topN(freq, 5).forEach(e ->
System.out.printf(" %-15s : %d%n", e.getKey(), e.getValue()));

// Using merge for manual counting (alternative approach)
Map<String, Integer> manualCount = new HashMap<>();
for (String word : text.toLowerCase().split("\\W+")) {
if (!word.isEmpty()) {
manualCount.merge(word, 1, Integer::sum);
}
}
System.out.println("\n'java' appears: " + manualCount.get("java") + " times");
}
}

7. Practical Example: Student Grade Grouping

import java.util.*;
import java.util.stream.*;

public class GradeGrouper {

record Student(String name, int score) {}

public static String getGrade(int score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
if (score >= 60) return "D";
return "F";
}

public static void main(String[] args) {
List<Student> students = List.of(
new Student("Alice", 95),
new Student("Bob", 82),
new Student("Charlie", 76),
new Student("Diana", 91),
new Student("Eve", 68),
new Student("Frank", 85),
new Student("Grace", 93)
);

// Group by grade using streams
Map<String, List<String>> byGrade = students.stream()
.collect(Collectors.groupingBy(
s -> getGrade(s.score()),
TreeMap::new, // sorted by grade (A, B, C...)
Collectors.mapping(Student::name, Collectors.toList())
));

byGrade.forEach((grade, names) ->
System.out.println("Grade " + grade + ": " + names));

// Score statistics per grade
Map<String, IntSummaryStatistics> stats = students.stream()
.collect(Collectors.groupingBy(
s -> getGrade(s.score()),
Collectors.summarizingInt(Student::score)
));

System.out.println("\nGrade Statistics:");
stats.forEach((grade, s) ->
System.out.printf(" Grade %-2s | Count: %d | Avg: %.1f | Range: %d-%d%n",
grade, s.getCount(), s.getAverage(), s.getMin(), s.getMax()));
}
}

Pro Tips

getOrDefault vs null check:

Map<String, Integer> map = new HashMap<>();
map.put("a", 1);

// Old way (verbose)
Integer val = map.get("b");
int result = (val != null) ? val : 0;

// Modern way (clean)
int result = map.getOrDefault("b", 0);

computeIfAbsent for grouping:

// Classic grouping pattern using computeIfAbsent
Map<String, List<String>> groups = new HashMap<>();
String key = "fruits";
groups.computeIfAbsent(key, k -> new ArrayList<>()).add("apple");
// Equivalent to:
// groups.putIfAbsent(key, new ArrayList<>());
// groups.get(key).add("apple");
// But computeIfAbsent is one line and doesn't create unnecessary lists

equals/hashCode for custom key objects:

When using a custom class as a Map key, always override both equals() and hashCode()(or use record). Without this, two logically equal keys will be treated as different, causing phantom entries.