Java Equality Operators: == vs equals()
java equality operators: Understand how == and equals() behave for primitives, objects, and Strings in Java, and learn to avoid common equality pitfalls.
Java equality operators are a common source of confusion, especially for developers moving from languages where == compares values. In Java, == compares references for objects and values for primitives, while equals() is a method that can be overridden to define logical equality. Understanding when each applies is essential for writing correct code.
The Difference Between == and equals() for Objects
For object types, == checks reference identity: two variables are equal only if they point to the exact same object in memory. The equals() method, on the other hand, is designed to check logical equality, which by default falls back to reference identity unless a class overrides it.
String a = new String("hello"); String b = new String("hello"); System.out.println(a == b); // false, different references System.out.println(a.equals(b)); // true, same content
The first comparison returns false because a and b are two distinct objects. The second returns true because String overrides equals() to compare the character sequence. This is the core distinction that drives most equality-related bugs in Java.
Equality for Primitives vs. Reference Types
Primitive types (int, double, boolean, etc.) are compared with == based on their actual values. There is no equals() method for primitives because they are not objects. Reference types, including wrappers like Integer, are compared with == based on reference identity unless you explicitly call equals().
int x = 42; int y = 42; System.out.println(x == y); // true, values match Integer boxedX = 42; Integer boxedY = 42; System.out.println(boxedX == boxedY); // true due to Integer cache, but not guaranteed for all values System.out.println(boxedX.equals(boxedY)); // true, always correct
The Integer cache holds values from -128 to 127, so == may return true for small numbers. For values outside that range, == compares references and can return false even when the numeric values are identical. Always use equals() or unbox to a primitive when comparing wrapper objects.
How String Equality Works
Strings are objects, but Java's string pool complicates == comparisons. String literals are interned, meaning the JVM reuses the same instance for identical literals. As a result, == often works for literals but fails when strings are created dynamically.
String literal1 = "hello"; String literal2 = "hello"; System.out.println(literal1 == literal2); // true, same pooled instance String dynamic = new String("hello"); System.out.println(literal1 == dynamic); // false, different instances System.out.println(literal1.equals(dynamic)); // true, same content
Relying on string interning is fragile. Code that works during development may break when strings come from user input, file reads, or concatenation. Always use equals() for string content comparison, and use equalsIgnoreCase() when case should be ignored.
Overriding equals() and hashCode() Together
When you define a custom class and need logical equality, you must override both equals() and hashCode(). The contract states that if two objects are equal according to equals(), they must produce the same hash code. Violating this breaks collections that rely on hashing, such as HashSet and HashMap.
public class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; return age == person.age && name.equals(person.name); } @Override public int hashCode() { return Objects.hash(name, age); } }
The equals() method first checks reference identity for a quick true, then verifies the type, and finally compares the fields. The hashCode() method uses Objects.hash() to generate a hash based on the same fields. This ensures that equal objects always have the same hash code, which is required for correct behavior in hash-based collections.
Common Pitfalls with == on Objects
One frequent mistake is using == to compare Integer objects outside the cache range. Another is comparing strings that are not interned. Enums are the notable exception: == is safe and recommended for enum constants because each constant is a singleton.
enum Color { RED, GREEN, BLUE } Color c1 = Color.RED; Color c2 = Color.RED; System.out.println(c1 == c2); // true, same singleton
For enums, == is both correct and more efficient than equals() because it avoids a method call. For all other objects, use equals() unless you specifically intend to check reference identity, such as when comparing a variable to itself or when implementing a cache key that must be the same instance.
Using Objects.equals() and Other Utility Methods
Writing null-safe equality checks manually is repetitive. The Objects.equals() utility method handles nulls gracefully: it returns true if both arguments are null, false if exactly one is null, and otherwise delegates to the first argument's equals() method.
String a = null; String b = "hello"; System.out.println(Objects.equals(a, b)); // false System.out.println(Objects.equals(null, null)); // true
For arrays, use Arrays.equals() for one-dimensional arrays and Arrays.deepEquals() for nested arrays. The default equals() on an array compares references, not contents, so == and equals() behave the same on arrays unless you use the utility methods.
Performance and Maintainability Considerations
Implementing equals() and hashCode() efficiently matters in performance-sensitive code. A naive hashCode() that recalculates expensive fields on every call can degrade hash-based collection performance. Caching the hash code in a field is a common optimization when the object is immutable and the hash is frequently used.
public class Point { private final int x; private final int y; private int cachedHash; public Point(int x, int y) { this.x = x; this.y = y; } @Override public int hashCode() { if (cachedHash == 0) { cachedHash = Objects.hash(x, y); } return cachedHash; } }
Caching is only safe for immutable objects. If the fields can change, the cached hash becomes stale and breaks the contract. For mutable objects, recalculating is the only correct approach, so weigh the cost of computation against the frequency of hash usage.
Equality in Collections and Maps
HashSet and HashMap rely on hashCode() to locate buckets and equals() to resolve collisions. If you insert a custom object into a HashSet without overriding both methods, the set will treat distinct instances as different even if their fields match.
Set<Person> people = new HashSet<>(); people.add(new Person("Alice", 30)); people.add(new Person("Alice", 30)); System.out.println(people.size()); // 1 if equals/hashCode are overridden, 2 otherwise
With proper overrides, the set deduplicates logically equal objects. Without them, each new creates a distinct reference, so the set grows. This behavior is a common source of subtle bugs when developers forget to implement equality for domain objects.
When == Is the Right Choice
Despite the pitfalls, == has legitimate uses. It is the correct operator for comparing enum constants, checking if two variables refer to the same object (e.g., in identity-based caches), and comparing primitives. It is also faster than equals() because it does not involve a method call, though the difference is negligible in most applications.
For reference types where identity is the intended semantics, == is not a mistake—it is the precise tool. The key is to know which equality you need: value equality for domain objects, or identity equality for low-level reference checks. Choosing the wrong operator leads to bugs that are hard to trace because the behavior often appears correct in simple tests but fails under different input sources or JVM optimizations.
When designing a class, decide early whether logical equality is meaningful. If it is, override equals() and hashCode() together and document the contract. If identity is sufficient, leave the default behavior and avoid exposing objects in collections that assume value semantics. This decision shapes how the rest of the codebase interacts with your objects.