Map in Java: HashMap, LinkedHashMap and TreeMap

A Map stores key to value pairs with unique keys, and it is the collection you will reach for most often.

What a Map is

A Map associates each unique key with one value. It is not a Collection, because it stores pairs rather than elements, but it offers collection views of its keys, values and entries.

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

wordCounts.put("java", 12);
wordCounts.put("sql", 5);
wordCounts.put("java", 15);            // replaces the previous value

System.out.println(wordCounts.get("java"));            // 15
System.out.println(wordCounts.get("missing"));         // null
System.out.println(wordCounts.getOrDefault("x", 0));   // 0
System.out.println(wordCounts.containsKey("sql"));
System.out.println(wordCounts.size());

wordCounts.remove("sql");

The implementations

HashMapLinkedHashMapTreeMapHashtable
Key orderNoneInsertion or accessSorted by keyNone
get and putO(1) averageO(1) averageO(log n)O(1) average
Null keyOne allowedOne allowedNot allowedNot allowed
Null valuesAllowedAllowedAllowedNot allowed
Thread safeNoNoNoYes, but legacy

Use HashMap by default. For thread safety use ConcurrentHashMap, never Hashtable.

Iterating

Map<String, Integer> counts = Map.of("java", 15, "sql", 5);

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

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

for (String key : counts.keySet()) { }
for (Integer value : counts.values()) { }

Prefer entrySet() when you need both. Looping over keySet() and calling get for each key performs a second lookup every time.

The methods that remove boilerplate

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

// Count occurrences
counts.merge("java", 1, Integer::sum);
counts.merge("java", 1, Integer::sum);          // now 2

// Insert only if absent
counts.putIfAbsent("sql", 0);

// Compute a value lazily
Map<String, List<String>> byLetter = new HashMap<>();
byLetter.computeIfAbsent("j", key -> new ArrayList<>()).add("java");

// Update an existing value
counts.computeIfPresent("java", (key, value) -> value * 10);

// Remove when the function returns null
counts.compute("sql", (key, value) -> value == 0 ? null : value);
// The old way, three lines and a lookup too many
List<String> list = byLetter.get("j");
if (list == null) {
    list = new ArrayList<>();
    byLetter.put("j", list);
}
list.add("java");
computeIfAbsent and merge are the two most useful map methods added in Java 8. Grouping and counting become one line each.

LinkedHashMap

Map<String, String> ordered = new LinkedHashMap<>();
ordered.put("first", "a");
ordered.put("second", "b");
System.out.println(ordered);      // predictable insertion order
// A least recently used cache in a few lines
Map<String, String> cache = new LinkedHashMap<>(16, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > 100;
    }
};

The third constructor argument switches to access order, so reading an entry moves it to the end. Overriding removeEldestEntry then evicts the least recently used one.

TreeMap and navigation

NavigableMap<LocalDate, Integer> visits = new TreeMap<>();
visits.put(LocalDate.of(2026, 1, 10), 120);
visits.put(LocalDate.of(2026, 2, 14), 340);
visits.put(LocalDate.of(2026, 3, 2), 260);

System.out.println(visits.firstKey());
System.out.println(visits.lastEntry());
System.out.println(visits.floorKey(LocalDate.of(2026, 2, 20)));
System.out.println(visits.headMap(LocalDate.of(2026, 3, 1)));
System.out.println(visits.descendingMap());

Range queries are the reason to choose TreeMap. A HashMap cannot answer "all entries before this date" without scanning everything.

Keys must be stable

Map<List<String>, String> risky = new HashMap<>();
List<String> key = new ArrayList<>(List.of("a"));
risky.put(key, "value");

key.add("b");                              // the hash changed
System.out.println(risky.get(key));        // null

Keys must be immutable, or at least never modified while in the map. String, the wrappers, enums, LocalDate and records are all good keys.

A practical example

public static Map<String, Integer> wordFrequency(String text) {
    Map<String, Integer> counts = new LinkedHashMap<>();

    for (String word : text.toLowerCase().split("[^a-z]+")) {
        if (!word.isBlank()) {
            counts.merge(word, 1, Integer::sum);
        }
    }
    return counts;
}

Common mistakes

  • Unboxing a null from get into an int.
  • Using a mutable object as a key.
  • Relying on HashMap iteration order.
  • Iterating keySet() and calling get for every key.
  • Adding or removing entries while iterating, instead of using entrySet().removeIf(...) or an iterator.
  • Choosing Hashtable for thread safety.

Best practices

  • Default to HashMap; use LinkedHashMap for predictable order and TreeMap for sorting or ranges.
  • Use getOrDefault, merge and computeIfAbsent instead of manual null checks.
  • Keep keys immutable.
  • Iterate entrySet() when both parts are needed.
  • Use Map.of or Map.copyOf for fixed maps, and ConcurrentHashMap when shared.

Practice

  1. Count word occurrences in a sentence in one statement using merge.
  2. Group a list of names by first letter using computeIfAbsent.
  3. Why does int n = map.get("missing"); throw, and what is the fix?
  4. Build a size limited cache with LinkedHashMap in access order.
  5. Use a TreeMap to find every entry between two dates.

Conclusion

A map is the workhorse of Java collections. Keep keys immutable, choose the implementation from the ordering you need, and learn merge and computeIfAbsent, which remove most of the null checking people still write by hand.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.