Back to Blog
Java

Java Object Identity vs Equality

java object identity vs equality: Understand the difference between reference identity and logical equality in Java, and how to correctly override equals() and hashCod...

Javaobject equalityidentityequals()hashCode()
Two Java object references pointing to separate memory locations while their equals() method considers them equal, illustrating identity versus equality.

When comparing two objects in Java, the result depends on whether you ask about identity or equality. Identity asks whether two references point to the same object in memory. Equality asks whether two distinct objects represent the same logical value. The distinction is fundamental, and getting it wrong leads to subtle bugs in collections, caching, and data validation. This article explains java object identity vs equality in concrete terms, with code examples and practical guidance for implementing correct behavior.

Reference Identity: The Default Behavior

Java's == operator compares references, not object contents. For any two reference variables, a == b evaluates to true only when both variables point to the exact same object instance. This is called identity comparison.

String a = new String("hello"); String b = new String("hello"); System.out.println(a == b); // false

Here, a and b are two separate String objects that contain the same character sequence. The == operator returns false because the references point to different memory locations. If you assign b = a, then a == b would be true.

Identity comparison is fast because it only compares the reference values. It does not inspect the object's fields or state. That speed is useful when you need to know whether two variables alias the same instance, for example when tracking object lifecycle or implementing reference-based caches.

Logical Equality: The equals() Method

The Object class defines public boolean equals(Object obj). The default implementation uses identity: return this == obj;. That means unless a class overrides equals(), two distinct instances are never considered equal. Many standard library classes, such as String, Integer, and ArrayList, override equals() to compare their internal state.

String a = new String("hello"); String b = new String("hello"); System.out.println(a.equals(b)); // true

The String class compares the character sequence, so a.equals(b) returns true even though a and b are different objects. This is logical equality: the objects are considered equal because they represent the same value.

Why the Distinction Matters

Mixing identity and equality can break code that relies on value semantics. Consider a HashSet that stores Person objects. If Person does not override equals(), the set uses identity, so adding two Person objects with the same name and age creates two separate entries. That is usually not what you want for a domain object.

class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } } Set<Person> people = new HashSet<>(); people.add(new Person("Alice", 30)); people.add(new Person("Alice", 30)); System.out.println(people.size()); // 2

Because Person uses the default equals(), the set treats the two instances as different. To make the set treat them as the same logical person, you must override equals() (and hashCode()) in Person.

Overriding equals() Correctly

The contract for equals() is defined in the Java documentation. It requires reflexivity, symmetry, transitivity, consistency, and a non-null result. In practice, you should follow a standard pattern:

@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Person other = (Person) obj; return age == other.age && Objects.equals(name, other.name); }

This implementation first checks identity for a quick exit, then checks for null and type compatibility, then compares the relevant fields. Using getClass() to check the type is stricter than using instanceof because it prevents a Person from being equal to a subclass instance. That is usually the right choice for value objects.

The hashCode() Contract

Whenever you override equals(), you must also override hashCode(). The contract states that equal objects must have equal hash codes. If you violate this, hash-based collections like HashMap and HashSet will behave unpredictably.

@Override public int hashCode() { return Objects.hash(name, age); }

Objects.hash computes a hash from the provided fields. It is concise, but it boxes primitives and may be slower than a hand-written implementation for hot paths. For most applications, it is perfectly fine. For performance-sensitive code, you can compute a custom hash using prime multipliers, but correctness matters more than micro-optimization.

Comparing Primitive Types and Wrappers

Primitive types like int and double use == for value comparison. Their wrapper classes like Integer and Double also override equals(), but using == on wrappers compares references, not values. This is a common source of confusion.

Integer x = 127; Integer y = 127; System.out.println(x == y); // true, due to integer cache Integer p = 128; Integer q = 128; System.out.println(p == q); // false

The first comparison returns true because Java caches Integer values from -128 to 127, so both references point to the same cached instance. The second comparison returns false because 128 is outside the cache and each autoboxing creates a new instance. The behavior depends on the JVM's cache range, which is not guaranteed by the language spec. Never rely on == for wrapper comparison; use equals() or unbox to primitives.

Performance and Runtime Cost

Identity comparison is a single reference check, essentially free. Equality comparison can be arbitrarily expensive depending on how many fields you compare and how deep the object graph is. For simple value objects, the cost is small, but for large collections or high-frequency operations, it can matter.

When you put objects in a HashMap, the map first calls hashCode() to find the bucket, then equals() to resolve collisions. If hashCode() is poorly implemented, many objects land in the same bucket, making equals() calls frequent. That can degrade lookup from O(1) toward O(n) in the the worst case.

A well-designed hashCode() spreads objects across buckets. It should use the same fields as equals(). If you change the fields used in equals() but not hashCode(), you break the contract and cause subtle collection failures.

Common Mistakes and Edge Cases

One frequent mistake is using instanceof in equals() without considering symmetry. If a subclass overides equals() and a superclass instance is compared with a subclass instance, the results can be asymmetric. The safer approach is to require the exact same class, as shown earlier.

Another issue is comparing arrays. Arrays inherit Object.equals(), so they use identity. To compare array contents, use Arrays.equals() or Arrays.deepEquals() for nested arrays.

int[] a = {1, 2, 3}; int[] b = = {1, 2, 3}; System.out.println(a.equals(b)); // false System.out.println(Arrays.equals(a, b)); // true

For List and other collection classes, equality is defined by the element order and the elements' own equals() methods. That means two ArrayList instances with the same elements in the same order are equal, regardless of their internal capacity or implementation details.

Choosing Between Identity and Equality in Your Code

Use identity when you are managing object lifecycles, such as tracking unique instances in a session or debugging reference leaks. Use equality when you are comparing domain values, such as checking whether two Order objects represent the same order.

A practical rule: if your class represents a value that can be duplicated without changing its meaning, override equals() and hashCode(). If your class represents a unique entity with an intrinsic identity (like a thread or a a database connection), keep the default identity semantics.

When you override equals(), also consider whether the class should be immutable. Mutable objects that are keys in hash collections can break if their state changes after insertion. The hash code changes, but the bucket does not, so the key becomes unreachable. Prefer immutable value objects for map keys.

Final Technical Consideration: Inheritance and Equality

The interaction between inheritance and equality is tricky. If a subclass adds a field that should affect equality, the parent's equals() cannot know about it. The common solution is to make the parent class final or to use composition instead of inheritance. If you must extend a class that overrides equals(), you can only safely add fields if the subclass never appears in a collection that relies on equality, or if you accept that the subclass instances will be equal to parent instances with the same fields.

In practice, many Java developers prefer to mark value classes as final to avoid these complications. That guarantees that getClass() checks in equals() are stable and that no subclass can violate the contract.

java object identity vs equality: Practical Usage and Code E | RYUSLOG DEV