6.4 The Map Interface, Sorting, Comparable, and Comparator
Key Takeaways
Map implementations manage key-value pairs outside the Collection hierarchy: HashMap provides O(1) hash bucket lookups, LinkedHashMap preserves insertion/access order, and TreeMap guarantees O(log n) key-sorted traversal.
Keys in hash-based collections must adhere strictly to the equals and hashCode contract; mutating key state after insertion alters the hash code and permanently strands entries in unreachable buckets.
Modern Map functional methods (computeIfAbsent, computeIfPresent, compute, merge) execute atomic updates; returning null from a remapping function removes the corresponding entry from the map.
Comparable<T>defines intrinsic natural ordering via int compareTo(T o), which must remain consistent with equals() to prevent TreeSet and TreeMap from discarding non-identical elements as duplicates.Comparator<T>provides external custom ordering through int compare(T o1, T o2) and fluent combinators (comparing, thenComparing, reversed, nullsFirst, nullsLast).
6.4 The Map Interface, Sorting, Comparable, and Comparator
Mapping keys to values and establishing precise sort orders are foundational operations in enterprise Java development. The Map<K, V> interface provides key-value associations without extending Collection. On the 1Z0-829 exam, questions frequently test the inner workings of hash bucket resolution, the strict equals() and hashCode() contract, modern lambda-enabled Map methods (merge, computeIfAbsent), and complex multi-tiered sorting using Comparable and Comparator combinators.
The Map Interface and Implementations
A Map<K, V> maps unique keys to values, prohibiting duplicate keys. Its primary implementations offer distinct performance and ordering trade-offs:
1. HashMap
- Internal Structure: An array of hash buckets. Collisions are stored in linked lists; when a bucket exceeds 8 entries and total table capacity reaches at least 64, the bucket transforms into a balanced red-black tree (
TreeNode) to guarantee worst-case lookup rather than . - Null Policy: Allows one
nullkey and multiplenullvalues. - Ordering: No ordering guarantees; bucket order shifts when resizing.
2. LinkedHashMap
- Internal Structure: Extends
HashMapwhile maintaining a doubly-linked list through all entries. - Ordering Modes: Defaults to insertion order. Can be configured via its constructor (
new LinkedHashMap<>(16, 0.75f, true)) to track access order, making it an ideal foundation for Least-Recently-Used (LRU) caches when combined withremoveEldestEntry().
3. TreeMap
- Internal Structure: A Red-Black tree implementing
NavigableMap<K, V>. - Ordering: Keys are sorted according to natural ordering (
Comparable) or an explicitComparator. - Null Policy: Prohibits
nullkeys when using natural ordering (throwsNullPointerException). Allowsnullvalues. - Performance: for
get,put,remove, andcontainsKey.
NavigableMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(10, "Ten");
treeMap.put(20, "Twenty");
treeMap.put(30, "Thirty");
System.out.println(treeMap.lowerKey(20)); // 10 (greatest key < 20)
System.out.println(treeMap.floorKey(20)); // 20 (greatest key <= 20)
System.out.println(treeMap.ceilingKey(25)); // 30 (least key >= 25)
System.out.println(treeMap.higherKey(20)); // 30 (least key > 20)
The equals() and hashCode() Contract
Hash-based collections (HashMap, HashSet) use an object's hashCode() to determine its bucket index, and equals() to resolve bucket collisions. The contract mandates:
- If
a.equals(b)istrue, thena.hashCode() == b.hashCode()must betrue. - If
a.hashCode() == b.hashCode(),a.equals(b)is not required to betrue(hash collision). - If
a.equals(b)isfalse, hash codes are not required to be distinct, though unique hash codes optimize performance.
The Mutable Key Hazard
If an object used as a map key is mutated such that its hashCode() changes, the entry becomes permanently lost:
class AccountKey {
int accountId;
AccountKey(int id) { this.accountId = id; }
public boolean equals(Object o) {
return o instanceof AccountKey k && this.accountId == k.accountId;
}
public int hashCode() { return accountId; }
}
Map<AccountKey, String> map = new HashMap<>();
AccountKey key = new AccountKey(101);
map.put(key, "Active");
// Mutate key field after insertion
key.accountId = 999;
// Lookup computes hashCode 999, searching the wrong bucket!
System.out.println(map.get(key)); // Prints null! Entry is stranded.
Warning
Always use immutable types (such as String, Integer, or Java 16 record types) as map keys to guarantee hash code stability.
Modern Functional Map Methods (Java 8+)
Java 8 introduced powerful methods on Map that accept lambdas for atomic modifications:
1. getOrDefault(key, defaultValue)
Returns the value mapped to key, or defaultValue if the key is not found or is explicitly mapped to null.
2. putIfAbsent(key, value)
Inserts value only if key is absent or currently mapped to null. Returns the existing value, or null if the new value was placed.
3. computeIfAbsent(key, k -> compute(k))
If key is absent or mapped to null, evaluates the mapping function. If the function returns non-null, inserts the result and returns it. If the function returns null, no entry is created.
4. computeIfPresent(key, (k, v) -> compute(k, v))
If key is present and mapped to a non-null value, executes the remapping function with (key, oldValue).
- If the function returns non-null, updates the entry.
- CRITICAL TRAP: If the function returns
null, the entry is removed from the map!
5. compute(key, (k, v) -> compute(k, v))
Executes the remapping function regardless of whether the key exists (v will be null if absent).
- If the function returns non-null, inserts or updates the entry.
- If the function returns
null, removes the entry (or leaves it absent).
6. merge(key, value, (oldVal, newVal) -> remap)
The merge method has the most surprising rules of the group. It takes a key, a non-null value, and a BiFunction remapping function:
- If
keyis absent or mapped tonull: associateskeydirectly withvalue. The remapping function is never invoked! - If
keyis present with non-nulloldVal: invokesremappingFunction.apply(oldVal, value).- If the function returns non-null, updates the key with the returned result.
- CRITICAL TRAP: If the function returns
null, the key is removed from the map!
- If the
valueargument passed tomergeisnull, it throwsNullPointerExceptionimmediately.
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 50);
// 1. Existing key: invokes lambda (90 + 10 = 100)
scores.merge("Alice", 10, (oldVal, newVal) -> oldVal + newVal); // Alice=100
// 2. Absent key: assigns 75 directly without calling lambda
scores.merge("Carol", 75, (oldVal, newVal) -> oldVal + newVal); // Carol=75
// 3. Lambda returns null: REMOVES key from map!
scores.merge("Bob", 50, (oldVal, newVal) -> null); // Bob is removed!
System.out.println(scores); // Prints {Alice=100, Carol=75}
Sorting with Comparable<T> vs. Comparator<T>
Java provides two interfaces to define order:
| Dimension | Comparable<T> | Comparator<T> |
|---|---|---|
| Package | java.lang | java.util |
| Definition Site | Defined within the domain class itself | Defined externally as separate classes or lambdas |
| Abstract Method | int compareTo(T other) | int compare(T o1, T o2) |
| Role | Natural intrinsic ordering | Custom, multiple, or dynamic sort orders |
| Sorting Invocations | Collections.sort(list) | list.sort(comparator) / Collections.sort(list, comparator) |
The compareTo Contract and TreeSet/TreeMap
The compareTo(T o) method returns:
- A negative integer if
this < o - Zero if the two objects are equal in sort order (ideally only when
this.equals(o)is alsotrue) - A positive integer if
this > o
Warning
The TreeSet/TreeMap Consistency with Equals Trap
TreeSet and TreeMap determine element equality and uniqueness using compareTo() (or compare()), not equals()! If a.compareTo(b) == 0, a TreeSet treats b as an exact duplicate of a and discards it—even if a.equals(b) is false! Always ensure natural ordering is consistent with equals().
Consider the following code snippet:
Map<String, Integer> map = new HashMap<>();
map.put("A", 10);
map.put("B", 20);
map.merge("A", 5, (oldVal, newVal) -> oldVal + newVal);
map.merge("B", 10, (oldVal, newVal) -> null);
map.merge("C", 15, (oldVal, newVal) -> oldVal + newVal);
System.out.println(map);
What is the state of the map after executing this code?
{A=15, C=15}
Throws a NullPointerException at runtime
{A=15, B=null, C=15}
{A=15, B=30, C=15}
Consider the following class and code snippet:
class Item implements Comparable<Item> {
int id;
String name;
Item(int id, String name) { this.id = id; this.name = name; }
@Override
public int compareTo(Item other) {
return Integer.compare(this.id, other.id);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Item item)) return false;
return id == item.id && Objects.equals(name, item.name);
}
@Override
public int hashCode() { return Objects.hash(id, name);
}
}
// Inside main:
Set<Item> set = new TreeSet<>();
set.add(new Item(1, "Pen"));
set.add(new Item(1, "Pencil"));
System.out.println(set.size());
What is the output of running this code?
Throws a ClassCastException at runtime
2
Throws an IllegalArgumentException at runtime
1
Consider the following code that sorts a list of employee records using chained comparators:
record Employee(String dept, String name, Integer score) {}
List<Employee> list = new ArrayList<>(List.of(
new Employee("Sales", "Bob", 80),
new Employee("IT", "Carol", null),
new Employee("IT", "Alice", 95),
new Employee("Sales", "Dan", 90)
));
Comparator<Employee> comp = Comparator.comparing(Employee::dept)
.thenComparing(Employee::score, Comparator.nullsFirst(Comparator.naturalOrder()))
.reversed();
list.sort(comp);
System.out.println(list.get(0).name());
Which employee's name is printed?
Bob
Alice
Carol
Dan
Sections you finish are checked off in the contents.