equals defines whether two objects represent the same value, while hashCode supplies the hash used by hash-based collections to narrow a lookup.
Java equals and hashCode: stable value keys
Make identity explicit
Two objects can represent the same shipment identifier while occupying different allocations. Reference equality checks whether they are the same object. Value equality checks the domain fields you choose. Mixing those questions is a common source of duplicate keys.
The key below uses carrier code and tracking number together. A tracking number alone might not be globally unique. Both fields are final strings, and the constructor rejects null, so the equality decision remains stable after insertion.
The equals contract requires reflexivity, symmetry, transitivity, consistency, and a false result against null. Equal objects must have equal hash codes. The converse is false: unequal objects may collide, and a HashMap must still compare them correctly.
Keep inheritance out of a value key
An extensible value class can make symmetry difficult if a subclass adds another equality field. The example uses a final class and compares only its own declared value type. This avoids promising equality across an inheritance hierarchy.
Objects.hash is readable but uses a varargs array and boxing for primitive arguments. For a hot key type, a direct hash calculation may reduce allocation. Measure before replacing a clear implementation with a hand-tuned one.
The map inserts one ShipmentKey and retrieves with a separately constructed equal key. That is the actual behavior a parsed request or imported row needs. Testing only the same reference would miss a broken value-equality implementation.
Working program
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ShipmentKeyLookup {
static final class ShipmentKey {
private final String carrier;
private final String tracking;
ShipmentKey(String carrier, String tracking) {
this.carrier = Objects.requireNonNull(carrier);
this.tracking = Objects.requireNonNull(tracking);
}
@Override
public boolean equals(Object candidate) {
if (this == candidate) return true;
if (!(candidate instanceof ShipmentKey)) return false;
ShipmentKey other = (ShipmentKey) candidate;
return carrier.equals(other.carrier) && tracking.equals(other.tracking);
}
@Override
public int hashCode() {
return 31 * carrier.hashCode() + tracking.hashCode();
}
}
public static void main(String[] args) {
Map<ShipmentKey, String> state = new HashMap<>();
state.put(new ShipmentKey("DX", "track-309"), "in-transit");
System.out.println(state.get(new ShipmentKey("DX", "track-309")));
System.out.println(state.size());
}
}Output
in-transit
1Cost and design choices
Equality can require comparing all relevant key text, so its work is proportional to those text lengths in the worst case. Hash-based collection lookup is expected constant bucket work only when hashes are well distributed; that statement does not remove the cost of computing a hash or checking equality.
Immutable fields prevent the inserted key from drifting to a new hash. Mutating a key can leave it physically stored under its old bucket decision while future lookups compute a different one.
Do not place a large mutable payload in the identity contract unless that is the actual meaning of the key. A smaller stable identifier reduces both comparison work and the risk of accidental identity changes.
Common Mistakes
- Override hashCode whenever value equals is overridden.
- Test equal but separately constructed keys, null, and unequal keys.
- Do not use array identity when the intended comparison is array contents.
- Do not assume final on a reference makes the referenced object immutable.
Connect the contracts
Compare the boundary explained in Set membership with the assumptions made by this program.
Compare the boundary explained in What final guarantees with the assumptions made by this program.
