Index

Advanced Map Concepts

Java Collections

8.1 Custom Key Classes and equals/hashCode contracts

When using objects as keys in Java Maps, especially in hash-based implementations like HashMap, correctly implementing the equals() and hashCode() methods is absolutely crucial. These two methods determine how keys are compared and how they are stored internally, directly affecting the behavior and performance of the map.

Why are equals() and hashCode() Important for Keys?

A HashMap uses a hash table data structure internally. When you put a key-value pair into a HashMap, the key’s hashCode() determines the bucket (a location in the table) where the entry is stored. When you later try to retrieve a value using the key, the hashCode() is used to locate the bucket quickly. Within that bucket, equals() is used to compare keys to find the exact matching entry.

If these methods are not implemented correctly:

The equals/hashCode Contract

Java’s official contract specifies:

Failing to honor these rules breaks the map’s ability to find entries correctly.

Common Mistakes

  1. Not overriding hashCode() when equals() is overridden: The default hashCode() from Object will use object identity, making logically equal objects have different hash codes.

  2. Using mutable fields for hashCode/equals: If the fields used to compute hashCode() or equals() change while the object is in a map, retrieval can fail.

  3. Ignoring the contract’s symmetry or transitivity: This can cause inconsistent behavior in collections.

Implementing equals() and hashCode() Step-by-Step

Consider a simple Person class with id and name:

public class Person {
    private final int id;
    private final String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // equals implementation
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;                // same reference
        if (obj == null || getClass() != obj.getClass()) return false;

        Person other = (Person) obj;
        return id == other.id &&                      // compare ids
               (name != null ? name.equals(other.name) : other.name == null);
    }

    // hashCode implementation
    @Override
    public int hashCode() {
        int result = Integer.hashCode(id);
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }
}

Explanation

What Happens Without Proper Implementation?

Person p1 = new Person(1, "Alice");
Person p2 = new Person(1, "Alice");

Map<Person, String> map = new HashMap<>();
map.put(p1, "Employee A");

System.out.println(map.get(p2));  // Without proper equals/hashCode, this prints null

Without overriding equals() and hashCode(), p2 is treated as a different key, even though it represents the same logical person, so the map lookup fails.

Summary

Correct implementation of these methods guarantees your custom key objects behave predictably and efficiently in maps, enabling robust and maintainable code.

Index

In Java’s Collections Framework, the SortedMap and NavigableMap interfaces extend the basic Map interface by providing sorted key order and powerful navigation methods. These interfaces allow you to work with maps that maintain their entries sorted by keys, which can be very useful when you need ordered views or need to efficiently query keys relative to others.

SortedMap Interface

SortedMap extends Map and guarantees that its keys are stored in ascending order, according to their natural ordering or a provided Comparator. This sorted order is reflected when you iterate over the keys or entries.

Key methods in SortedMap include:

NavigableMap extends SortedMap and adds methods for more detailed navigation around keys:

TreeMap: The Primary Implementation

The most common NavigableMap implementation is TreeMap. Internally, TreeMap uses a Red-Black tree data structure, a balanced binary search tree, to maintain sorted order and guarantee O(log n) time complexity for key lookups and updates.

Practical Example

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

public class NavigableMapExample {
    public static void main(String[] args) {
        NavigableMap<Integer, String> map = new TreeMap<>();

        map.put(10, "Ten");
        map.put(20, "Twenty");
        map.put(30, "Thirty");
        map.put(40, "Forty");

        System.out.println("Keys in ascending order:");
        for (Integer key : map.keySet()) {
            System.out.print(key + " ");
        }
        System.out.println("\n");

        System.out.println("floorKey(25): " + map.floorKey(25));   // 20
        System.out.println("ceilingKey(25): " + map.ceilingKey(25)); // 30
        System.out.println("lowerKey(20): " + map.lowerKey(20));     // 10
        System.out.println("higherKey(20): " + map.higherKey(20));   // 30

        // Using pollFirstEntry() removes and returns the smallest entry
        Map.Entry<Integer, String> firstEntry = map.pollFirstEntry();
        System.out.println("\nRemoved first entry: " + firstEntry.getKey() + " = " + firstEntry.getValue());

        System.out.println("Keys after removal:");
        for (Integer key : map.keySet()) {
            System.out.print(key + " ");
        }
    }
}

Output Explanation:

Summary

Understanding and leveraging NavigableMap and SortedMap unlocks advanced capabilities for ordered key-based data management in Java.

Index

8.3 WeakHashMap, IdentityHashMap, ConcurrentHashMap

In addition to the commonly used Map implementations like HashMap and TreeMap, the Java Collections Framework provides specialized Maps designed for specific scenarios. Understanding WeakHashMap, IdentityHashMap, and ConcurrentHashMap can help you choose the right tool for advanced use cases involving memory management, identity comparison, and concurrency.

WeakHashMap: Keys Held Weakly for Garbage Collection

A WeakHashMap holds its keys weakly, meaning that the presence of a key in the map does not prevent that key object from being garbage collected. When a key is no longer referenced elsewhere, it becomes eligible for garbage collection, and the entry is automatically removed from the map.

Use Case: WeakHashMap is useful for caching data where you want entries to disappear automatically when their keys are no longer in use elsewhere. This helps prevent memory leaks in caches or listeners.

Example scenario: Imagine caching metadata for objects that may be discarded — when those objects are no longer referenced, the cache entries should be cleaned up automatically.

IdentityHashMap: Key Equality by Reference

Unlike most Map implementations that use equals() to compare keys, IdentityHashMap uses reference equality (==) to compare keys. This means two keys are considered equal only if they are the exact same object instance.

Use Case: IdentityHashMap is useful when identity semantics matter, for example, when you want to track object references or use objects that do not properly override equals() and hashCode().

Example scenario: You might use IdentityHashMap in a serialization framework or object graph traversal where the distinction between different instances with identical contents is important.

ConcurrentHashMap: Thread-Safe, Lock-Free Map

ConcurrentHashMap is a thread-safe Map implementation designed for high concurrency. Unlike Hashtable (which synchronizes every method), ConcurrentHashMap uses lock striping and non-blocking algorithms to allow multiple threads to read and write without blocking each other unnecessarily.

Use Case: It is ideal in multi-threaded applications where multiple threads frequently read and write to a shared map concurrently, such as caches, session stores, or shared registries.

Key features:

Illustrative Examples

WeakHashMap Example

import java.util.WeakHashMap;

public class WeakHashMapDemo {
    public static void main(String[] args) {
        WeakHashMap<Object, String> map = new WeakHashMap<>();
        Object key = new Object();
        map.put(key, "Cached Value");

        System.out.println("Map before nulling key: " + map);
        key = null;  // Remove strong reference to key
        System.gc(); // Suggest garbage collection

        // After GC, the key-value entry may be removed automatically
        System.out.println("Map after GC: " + map);
    }
}

IdentityHashMap Example

import java.util.IdentityHashMap;

public class IdentityHashMapDemo {
    public static void main(String[] args) {
        IdentityHashMap<String, String> map = new IdentityHashMap<>();

        String a = new String("key");
        String b = new String("key");

        map.put(a, "Value A");
        map.put(b, "Value B");

        // Although a.equals(b) is true, they are different references
        System.out.println("Map size: " + map.size());  // Output: 2
    }
}

ConcurrentHashMap Example

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // Multiple threads can safely update this map concurrently
        map.put("Alice", 1);
        map.putIfAbsent("Bob", 2);

        System.out.println("ConcurrentHashMap: " + map);
    }
}

Summary

Map Type Key Equality Special Feature Typical Use Case
WeakHashMap Uses equals() Keys held weakly; auto removal on GC Caches, memory-sensitive mappings
IdentityHashMap Uses reference (==) Identity-based key comparison Object identity tracking, serialization
ConcurrentHashMap Uses equals(), thread-safe High concurrency, lock-free reads, atomic operations Multi-threaded shared maps, caches

By understanding these specialized Maps, you can better handle memory-sensitive caching, identity-based lookups, and concurrent access scenarios, all of which are common in real-world Java applications.

Index

8.4 Runnable Examples: Creating custom keys, using advanced maps

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class AdvancedMapExamples {

    // Custom class used as Map key - must override equals() and hashCode()
    static class Person {
        String name;
        int id;

        Person(String name, int id) {
            this.name = name;
            this.id = id;
        }

        // Proper equals: based on 'id' only for uniqueness
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Person)) return false;
            Person p = (Person) o;
            return id == p.id;
        }

        // hashCode consistent with equals
        @Override
        public int hashCode() {
            return Objects.hash(id);
        }

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

    public static void main(String[] args) throws InterruptedException {
        // 1. Using custom class as key in HashMap
        Map<Person, String> personMap = new HashMap<>();
        Person p1 = new Person("Alice", 101);
        Person p2 = new Person("Bob", 102);
        Person p3 = new Person("Alice Duplicate", 101); // Same ID as p1

        personMap.put(p1, "Engineer");
        personMap.put(p2, "Manager");
        personMap.put(p3, "Architect");  // Will overwrite p1's entry due to equals()

        System.out.println("Custom key HashMap:");
        personMap.forEach((k, v) -> System.out.println(k + " => " + v));
        // Output shows only two entries because p1 and p3 are equal by id

        // 2. Navigating TreeMap with NavigableMap methods
        NavigableMap<Integer, String> treeMap = new TreeMap<>();
        treeMap.put(10, "Ten");
        treeMap.put(20, "Twenty");
        treeMap.put(30, "Thirty");

        System.out.println("\nNavigableMap navigation:");
        System.out.println("Keys: " + treeMap.keySet());
        System.out.println("Lower key than 20: " + treeMap.lowerKey(20));   // 10
        System.out.println("Floor key of 25: " + treeMap.floorKey(25));     // 20
        System.out.println("Ceiling key of 25: " + treeMap.ceilingKey(25)); // 30
        System.out.println("Higher key than 20: " + treeMap.higherKey(20)); // 30

        // 3. WeakHashMap demo: entries removed when keys have no strong refs
        WeakHashMap<Object, String> weakMap = new WeakHashMap<>();
        Object key = new Object();
        weakMap.put(key, "Weak Value");

        System.out.println("\nWeakHashMap before GC: " + weakMap);
        key = null;           // Remove strong reference
        System.gc();          // Suggest GC, may clear weak entries
        Thread.sleep(100);    // Pause to allow GC (not guaranteed)

        System.out.println("WeakHashMap after GC (may be empty): " + weakMap);

        // 4. ConcurrentHashMap basic concurrent access
        ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
        concurrentMap.put("X", 1);
        concurrentMap.put("Y", 2);

        // Simulate concurrent update using lambda and atomic method
        concurrentMap.computeIfAbsent("Z", k -> 3);
        concurrentMap.merge("X", 10, Integer::sum);

        System.out.println("\nConcurrentHashMap contents:");
        concurrentMap.forEach((k, v) -> System.out.println(k + " => " + v));
        // Output: X => 11, Y => 2, Z => 3
    }
}

Explanation:

These examples highlight key advanced Map concepts and practical usage scenarios in Java Collections.

Index