1. Overview
In this tutorial, we’ll learn how to use the IdentityHashMap class in Java. We’ll also examine how it differs from the general HashMap class. Though this class implements the Map interface, it violates the contract of the Map interface.
For more detailed documentation, we can refer to the IdenityHashMap java doc page. For more details on the general HashMap class, we can read A Guide to Java HashMap.
2. About the IdentityHashMap Class
This class implements the Map interface. The Map interface mandates the use of the equals() method on the key comparison. However, the IdentityHashMap class violates that contract. Instead, it uses reference equality (==) on key search operations.
During search operations, HashMap uses the hashCode() method for hashing, whereas IdentityHashMap uses the System.identityHashCode() method. It also uses the linear probe technique of the hashtable for search operations.
The use of reference equality, System.identityHashCode(), and the linear probe technique give the IdentityHashMap class a better performance.
3. Using the IdentityHashMap Class
Object construction and method signatures are the same as HashMap, but the behavior is different due to reference equality.
3.1. Creating IdentityHashMap Objects
We can create it using the default constructor:
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>();
Or it can be created using the initial expected capacity:
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
If we don’t specify the initial expectedCapcity parameter as we did above, it uses 21 as the default capacity.
We can also create it using another map object:
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>(otherMap);
In this case, it initializes the created identityHashMap with the entries of otherMap.
3.2. Add, Retrieve, Update and Remove Entries
The put() method is used to add an entry:
identityHashMap.put("title", "Harry Potter and the Goblet of Fire");
identityHashMap.put("author", "J. K. Rowling");
identityHashMap.put("language", "English");
identityHashMap.put("genre", "Fantasy");
We can also add all of the entries from the other map using the putAll() method:
identityHashMap.putAll(otherMap);
To retrieve values, we use the get() method:
String value = identityHashMap.get(key);
To update a value for a key, we use the put() method:
String oldTitle = identityHashMap.put("title", "Harry Potter and the Deathly Hallows");
assertEquals("Harry Potter and the Goblet of Fire", oldTitle);
In the above snippet, the put() method returns the old value after the update. The second statement ensures that oldTitle matches the earlier “title” value.
We can use the remove() method to remove an element:
identityHashMap.remove("title");
3.3. Iterate Through All Entries
We can iterate through all the entries using the entitySet() method:
Set<Map.Entry<String, String>> entries = identityHashMap.entrySet();
for (Map.Entry<String, String> entry: entries) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
We can also iterate through all the entries using the keySet() method:
for (String key: identityHashMap.keySet()) {
System.out.println(key + ": " + identityHashMap.get(key));
}
These iterators use a fail-fast mechanism. If the map is modified while iterating, it throws a ConcurrentModificationException.
3.4. Other Methods
We also have different methods available that work similarly to other Map objects:
- clear(): removes all entries
- containsKey(): finds whether a key exists in the map or not. Only references are equated
- containsValue(): finds whether the value exists in the map. Only references are equated
- keySet(): returns an identity-based keyset
- size(): returns the number of entries
- values(): returns a collection of values
3.5. Support for Null Keys and Null Values
IdentityHashMap allows null for both the key and value:
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>();
identityHashMap.put(null, "Null Key Accepted");
identityHashMap.put("Null Value Accepted", null);
assertEquals("Null Key Accepted", identityHashMap.get(null));
assertEquals(null, identityHashMap.get("Null Value Accepted"));
The above snippet ensures null both as key and value.
3.6. Concurrency With IdentityHashMap
IdentityHashMap isn’t threadsafe, the same as HashMap. So if we have multiple threads to access/modify IdentityHashMap entries in parallel, we should convert them to the synchronized map.
We can get a synchronized map using the Collections class:
Map<String, String> synchronizedMap = Collections.synchronizedMap(new IdentityHashMap<String, String>());
4. Example Usage of Reference Equality
IdentityHashMap uses reference equality (==) over the equals() method to search/store/access key objects.
An IdentityHashMap created with four properties:
IdentityHashMap<String, String> identityHashMap = new IdentityHashMap<>();
identityHashMap.put("title", "Harry Potter and the Goblet of Fire");
identityHashMap.put("author", "J. K. Rowling");
identityHashMap.put("language", "English");
identityHashMap.put("genre", "Fantasy");
Another HashMap created with the same properties:
HashMap<String, String> hashMap = new HashMap<>(identityHashMap);
hashMap.put(new String("genre"), "Drama");
assertEquals(4, hashMap.size());
When using a new string object “genre” as a key, HashMap equates it with the existing key and updates the value. Hence, the size of the hash map remains the same as 4.
The following code snippet shows how IdentityHashMap behaves different:
identityHashMap.put(new String("genre"), "Drama");
assertEquals(5, identityHashMap.size());
IdentityHashMap considers the new “genre” string object as a new key. Hence, it asserts size to be 5. Two different objects of “genre” are used as two keys, with “Drama“ and “Fantasy“ as values.
5. Mutable Keys
IdentityHashMap allows mutable keys. This is yet another useful feature of this class.
Here we’ll take a simple Book class as a mutable object:
class Book {
String title;
int year;
// other methods including equals, hashCode and toString
}
First, two mutable objects of Book class are created:
Book book1 = new Book("A Passage to India", 1924);
Book book2 = new Book("Invisible Man", 1953);
Following code shows mutable key usage with HashMap:
HashMap<Book, String> hashMap = new HashMap<>(10);
hashMap.put(book1, "A great work of fiction");
hashMap.put(book2, "won the US National Book Award");
book2.year = 1952;
assertEquals(null, hashMap.get(book2));
Though the book2 entry is present in HashMap, it couldn’t retrieve its value. Because it has been modified and equals() method now doesn’t equate with the modified object. This is why general Map objects mandate immutable objects as a key.
The below snippet uses the same mutable keys with IdentityHashMap:
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
identityHashMap.put(book1, "A great work of fiction");
identityHashMap.put(book2, "won the US National Book Award");
book2.year = 1951;
assertEquals("won the US National Book Award", identityHashMap.get(book2));
Interestingly, IdentityHashMap is able to retrieve values even when the key object has been modified. In the above code, assertEquals ensures that the same text is retrieved again. This is possible due to reference equality.
6. Comparing Values
Up to Java 20, the IdentityHashMap had non-obvious behavior regarding values. While all the keys were compared by their identity, the values were still compared by their equality. This happened while using two methods: remove(Object, Object) and replace(Object, Object, Object).
Let’s check the first method. It removes an entry if both key and value match:
Book book = new Book("A Passage to India", 1924);
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
identityHashMap.put(book, "A great work of fiction");
identityHashMap.remove(book, new String("A great work of fiction"));
assertEquals(null, identityHashMap.get(book));
The second method is similar to the first one, but it replaces the value of the entry, where the key and the value match:
Book book = new Book("A Passage to India", 1924);
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
identityHashMap.put(book, "A great work of fiction");
identityHashMap.replace(book, new String("A great work of fiction"), "One of the greatest books");
assertEquals("One of the greatest books", identityHashMap.get(book));
The main issue is that the IdentityHashMap, until Java 20, didn’t override these methods and used default implementations from Map. Thus, it’s important to know about this issue, as upgrading to Java 20 and above might introduce undesired behavior and hard-to-debug bugs.
Overall the fix would produce the following result for the first example:
Book book = new Book("A Passage to India", 1924);
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
identityHashMap.put(book, "A great work of fiction");
identityHashMap.remove(book, new String("A great work of fiction"));
assertEquals("A great work of fiction", identityHashMap.get(book));
The following result would be produced for the second method:
Book book = new Book("A Passage to India", 1924);
IdentityHashMap<Book, String> identityHashMap = new IdentityHashMap<>(10);
identityHashMap.put(book, "A great work of fiction");
identityHashMap.replace(book, new String("A great work of fiction"), "One of the greatest books");
assertEquals("A great work of fiction", identityHashMap.get(book));
7. Some Use Cases
As a result of its features, IdentiyHashMap stands apart from other Map objects. However, it isn’t used for general purposes, and therefore we need to be cautious while using this class.
It’s helpful in building specific frameworks, including:
- Maintaining proxy objects for a set of mutable objects
- Building a quick cache based on an object reference
- Keeping an in-memory graph of objects with references
8. Conclusion
In this article, we learned how to work with IdentityHashMap, how it differs from general HashMap, and some use cases.
A complete code sample can be found over on GitHub.