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.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
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
HashMap | LinkedHashMap | TreeMap | Hashtable | |
|---|---|---|---|---|
| Key order | None | Insertion or access | Sorted by key | None |
| get and put | O(1) average | O(1) average | O(log n) | O(1) average |
| Null key | One allowed | One allowed | Not allowed | Not allowed |
| Null values | Allowed | Allowed | Allowed | Not allowed |
| Thread safe | No | No | No | Yes, 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");computeIfAbsentandmergeare 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)); // nullKeys 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
nullfromgetinto anint. - Using a mutable object as a key.
- Relying on
HashMapiteration order. - Iterating
keySet()and callinggetfor every key. - Adding or removing entries while iterating, instead of using
entrySet().removeIf(...)or an iterator. - Choosing
Hashtablefor thread safety.
Best practices
- Default to
HashMap; useLinkedHashMapfor predictable order andTreeMapfor sorting or ranges. - Use
getOrDefault,mergeandcomputeIfAbsentinstead of manual null checks. - Keep keys immutable.
- Iterate
entrySet()when both parts are needed. - Use
Map.oforMap.copyOffor fixed maps, andConcurrentHashMapwhen shared.
Practice
- Count word occurrences in a sentence in one statement using
merge. - Group a list of names by first letter using
computeIfAbsent. - Why does
int n = map.get("missing");throw, and what is the fix? - Build a size limited cache with
LinkedHashMapin access order. - Use a
TreeMapto 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.