Understanding Map Behavior in Java: Primitive Types vs Custom Object Keys
Tags: java, datastructures, effiectivejava
Published: 2024-01-05 17:35:54

**Introduction**
When using Java's Map collection, behavior differences can emerge based on whether you use primitive types (or their wrapper classes) as keys versus using your own custom classes. Let’s explore this phenomenon with two code examples – one using a primitive type (`int `and its wrapper class `Integer`) and another using a custom class. We’ll then look at the output of each and discuss why these approaches yield different results. Finally, we'll delve into a solution in line with the principles from "Effective Java" by Joshua Bloch.

**Experimenting with Maps**
**Using Primitive Type (int) and Its Wrapper (Integer) as Map Keys**
Firstly, let’s consider a map using `int `(and its wrapper class `Integer`) as keys:

```
Map<Integer, String> mapWithIntegers = new HashMap<>();
mapWithIntegers.put(1, "Apple");
mapWithIntegers.put(2, "Banana");

System.out.println(mapWithIntegers.get(1)); // Output: Apple
System.out.println(mapWithIntegers.get(2)); // Output: Banana

```
**Using a Custom Class as Map Keys**
Now, let's use a custom class `Fruit `as the key:

```
class Fruit {
    private String name;

    Fruit(String name) {
        this.name = name;
    }
    // getters and setters
}

Map<Fruit, String> mapWithCustomObjects = new HashMap<>();
mapWithCustomObjects.put(new Fruit("Apple"), "Red");
mapWithCustomObjects.put(new Fruit("Banana"), "Yellow");

System.out.println(mapWithCustomObjects.get(new Fruit("Apple"))); // Output: null
System.out.println(mapWithCustomObjects.get(new Fruit("Banana"))); // Output: null

```
**Observations**
In the first scenario, using integers as keys, the map retrieves the values correctly. However, in the second case, despite using seemingly identical keys (custom `Fruit `objects), the map returns `null`. Let's understand why.

**Behind the Scenes: Primitive Types and Wrapper Classes vs Custom Objects**

**Why Integer Works**
In Java, primitive types such as `int `are automatically wrapped into their corresponding wrapper classes (`Integer `in this case) when used in collections. These wrapper classes have `equals()` and `hashCode()` methods properly implemented. They compare and hash based on value, not reference, allowing the map to accurately retrieve values.

**Why the Custom Class Fails**
By default, our custom class Fruit inherits `equals()` and `hashCode()` from `Object`. These methods compare and hash based on object references. Hence, two different instances of Fruit with the same name are considered unequal, and their hash codes differ, leading to failed map lookups.

**The Solution: Implementing equals() and hashCode()**
Following the principles from "Effective Java", we need to override the `equals()` and `hashCode()` methods in our custom class.

**Why Override equals() and hashCode()?**
When you use a custom class as a key in a map, Java needs a way to determine if two keys are "equal" and to compute the storage location (hash) for each key. The default implementations of `equals()` and `hashCode()` in the Object class are often insufficient for custom objects. The default `equals()` method simply checks if two references point to the same object, and `hashCode()` returns a distinct integer for each object, even if they are "equal" in terms of their properties.


**Crafting the equals() Method**
According to "Effective Java", the `equals()` method must be:

- Reflexive: An object must equal itself.
- Symmetric: If A equals B, then B must equal A.
- Transitive: If A equals B and B equals C, then A must equal C.
- Consistent: Repeated calls must consistently return the same value.
- Non-nullity: Any non-null object should not equal null.

**The hashCode() Contract**
The `hashCode()` method must produce the same integer result for two objects that are equal according to `equals()`. It's not required to produce a unique hash code for each distinct object

**Overriding equals() and hashCode()**

```
class Fruit {
    private String name;

    Fruit(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Fruit fruit = (Fruit) o;
        return Objects.equals(name, fruit.name);
    }

   @Override
   public int hashCode() {
       int result = 17; // Initial non-zero value
       result = 31 * result +
               (name != null ? name.hashCode() : 0);
       return result;
   }
}

```
**Explanation of hashCode()**
**Why start with a non-zero constant (like 17)?**

  - Starting with a non-zero constant reduces the risk of ending 
    up with a hash code of 0 for distinct objects, which could 
    lead to poor performance in hash tables.

**Why multiply by 31?**
  - 31 is an odd prime number, chosen because it's an inexpensive 
    operation to perform (shift and subtract) and historically has 
    produced good distributions of hash codes.
  - Multiplying by 31 creates a nice mix of the hash code 
    bits,which tends to produce distinct hash codes for distinct 
    objects, reducing collisions in hash tables.

**Why add the result of name.hashCode()?**

  - The hash code of the `name `field is included to ensure that 
    different `Fruit `objects with different names produce different 
    hash codes, adhering to the contract of `hashCode()` that equal 
    objects must produce the same hash code, and unequal objects 
    should ideally produce distinct hash codes.

**Revisiting the Custom Class Example**
With the overridden methods, let's use our Fruit class again:

```
Map<Fruit, String> mapWithCustomObjects = new HashMap<>();
mapWithCustomObjects.put(new Fruit("Apple"), "Red");
mapWithCustomObjects.put(new Fruit("Banana"), "Yellow");

System.out.println(mapWithCustomObjects.get(new Fruit("Apple"))); // Output: Red
System.out.println(mapWithCustomObjects.get(new Fruit("Banana"))); // Output: Yellow

```
**Conclusion**
The key takeaway is the importance of properly implementing equals() and hashCode() in custom classes for use as keys in maps. These methods ensure that map keys are compared and hashed based on their content rather than their memory references. By adhering to the guidelines from "Effective Java," we ensure our custom objects behave predictably in maps, leading to consistent and reliable applications.

**REF:** "Effective Java" by Joshua Bloch.