Definition
The difference is identity versus content:
- == on primitives compares values directly.
5 == 5is true. This is correct and the only option. - == on objects compares references (memory addresses). Two different String objects with the same text are
==false because they are different objects. - .equals() on objects compares content.
String.equals()compares characters.Integer.equals()compares int values. Classes must override it to define what "equal content" means. - Default .equals() in Object uses
==(reference comparison). If you do not override it,.equals()is the same as==.
Two bank passbooks for Rajesh's account SAV2001. Using == asks: "Is this the exact same physical booklet?" Using .equals() asks: "Do both passbooks show the same account number and balance?" Two separate printouts are different booklets (== false) but describe the same account (.equals() true).
Understanding the Concepts
How does == work on primitives?
For primitives, == compares the actual values. This is correct and the only way to compare them.
int a = 42, b = 42;
System.out.println(a == b); // true (values are equal)
double x = 3.14, y = 3.14;
System.out.println(x == y); // true
boolean p = true, q = true;
System.out.println(p == q); // true
Primitives do not have an .equals() method. You must use ==.
How does == work on objects?
For objects, == compares references (memory addresses), not values.
String a = new String("SAV2001");
String b = new String("SAV2001");
System.out.println(a == b); // false (different objects in memory)
System.out.println(a.equals(b)); // true (same characters)
Even though both strings contain "SAV2001", they are two separate objects on the heap. == checks if both variables point to the exact same object, which they do not.
Why do String literals work with ==?
String literals are automatically interned into a shared pool. Two identical literals return the same pooled object.
String a = "SAV2001"; // interned (pool)
String b = "SAV2001"; // same interned object
System.out.println(a == b); // true (same object from pool)
System.out.println(a.equals(b)); // true (same characters)
String c = new String("SAV2001"); // new object on heap
System.out.println(a == c); // false (different object!)
System.out.println(a.equals(c)); // true (same characters)
System.out.println(a == c.intern()); // true (intern returns the pooled object)
Never rely on this. Strings from databases, user input, and network calls are not interned. Always use .equals().
How does .equals() work for BigDecimal?
BigDecimal has a trap: .equals() is scale-sensitive. Two amounts that represent the same value but have different scales are not .equals().
BigDecimal a = new BigDecimal("50000"); // scale 0
BigDecimal b = new BigDecimal("50000.00"); // scale 2
System.out.println(a.equals(b)); // false (different scale!)
System.out.println(a.compareTo(b) == 0); // true (same numeric value)
For money comparison, always use compareTo() == 0, not .equals().
How do you override equals() correctly?
When you create a domain class like Account, you must override .equals() to define what "equal" means for your objects.
public class Account {
private final String accountId;
private final String holderName;
public Account(String accountId, String holderName) {
this.accountId = accountId;
this.holderName = holderName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // same reference
if (!(o instanceof Account other)) return false; // type check (Java 16+)
return accountId.equals(other.accountId); // business key
}
@Override
public int hashCode() {
return accountId.hashCode(); // consistent with equals
}
}
The equals contract (from Object javadoc):
- Reflexive:
a.equals(a)is always true. - Symmetric: if
a.equals(b)thenb.equals(a). - Transitive: if
a.equals(b)andb.equals(c)thena.equals(c). - Consistent: same result every time if objects are unchanged.
- Null-safe:
a.equals(null)is always false.
Why must you override hashCode() together with equals()?
HashMap and HashSet use hashCode() to find the bucket, then .equals() to match the key. If two objects are .equals() but have different hash codes, HashMap cannot find them.
// WITHOUT overriding hashCode: HashMap breaks
Account a1 = new Account("SAV2001", "Rajesh");
Account a2 = new Account("SAV2001", "Rajesh");
Map<Account, BigDecimal> balances = new HashMap<>();
balances.put(a1, new BigDecimal("1000000"));
// a1.equals(a2) is true, but without hashCode override,
// they may land in different buckets
System.out.println(balances.get(a2)); // null! (cannot find it)
// WITH hashCode override: works correctly
System.out.println(balances.get(a2)); // 1000000
Rule: if you override equals(), you must override hashCode(). If two objects are equal, they must have the same hash code.
What about Objects.equals() for null safety?
a.equals(b) throws NullPointerException if a is null. Objects.equals() handles null on both sides.
import java.util.Objects;
String a = null;
String b = "SAV2001";
// a.equals(b); // NPE! a is null
System.out.println(Objects.equals(a, b)); // false (no NPE)
System.out.println(Objects.equals(null, null)); // true
System.out.println(Objects.equals(a, a)); // true
Use Objects.equals() when either side might be null.
What about enums?
For enums, == is correct and preferred. Each enum constant is a singleton, so there is only one instance of each value.
enum AccountType { SAVINGS, CURRENT, LOAN }
AccountType a = AccountType.SAVINGS;
AccountType b = AccountType.SAVINGS;
System.out.println(a == b); // true (same singleton)
System.out.println(a.equals(b)); // true (also works)
// == is preferred for enums: it is null-safe and faster
System.out.println(a == null); // false (no NPE)
// a.equals(null) also works but == is cleaner
What about arrays?
== on arrays compares references (are they the same array object?). To compare contents, use Arrays.equals().
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println(a == b); // false (different objects)
System.out.println(a.equals(b)); // false (Object.equals = reference)
System.out.println(Arrays.equals(a, b)); // true (compares elements)
// For nested arrays (2D), use Arrays.deepEquals()
int[][] x = {{1, 2}, {3, 4}};
int[][] y = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepEquals(x, y)); // true
What about Java records?
Records (Java 16+) auto-generate equals() and hashCode() based on all components.
public record AccountSummary(String accountId, BigDecimal balance) {}
var a = new AccountSummary("SAV2001", new BigDecimal("1000000"));
var b = new AccountSummary("SAV2001", new BigDecimal("1000000"));
System.out.println(a.equals(b)); // true (auto-generated, compares all components)
System.out.println(a == b); // false (different objects)
Records eliminate the need to manually write equals/hashCode for simple data carriers.
What are the complete rules?
| Comparison | Use | Why |
|---|---|---|
| Primitives (int, double, boolean) | == | Only option, compares values |
| Null check | == | x == null is idiomatic |
| Enums | == | Singletons, null-safe, fast |
| String content | .equals() | Compares characters |
| Integer/Long/wrapper content | .equals() | Compares wrapped value |
| BigDecimal value | compareTo() == 0 | Scale-independent |
| Custom objects | .equals() (override) | Define business equality |
| Arrays | Arrays.equals() | Element-by-element |
| Nullable references | Objects.equals() | Null-safe |
What are common mistakes?
- Using == for String comparison. Works for literals, fails for strings from DB/network/user input.
- Using .equals() for BigDecimal money comparison. Scale-sensitive:
50000is not.equals()to50000.00. UsecompareTo(). - Overriding equals() without hashCode(). HashMap and HashSet break silently.
- Calling .equals() on a null reference.
nullRef.equals(x)throws NPE. UseObjects.equals()or put the known non-null value on the left:"SAV2001".equals(maybeNull). - Using == for wrapper comparison. Works for Integer -128 to 127 (cached), fails for larger values.
What should a senior engineer know?
- Lombok @EqualsAndHashCode generates equals/hashCode. Use
@EqualsAndHashCode(of = "accountId")to base equality on the business key only. - JPA entities: use business key, not @Id. Generated IDs are null before persist. Base equals on a natural key like
accountId. - Immutable objects simplify equals. If fields never change, equals/hashCode are stable. Records enforce this.
- instanceof pattern matching (Java 16+) simplifies equals:
if (!(o instanceof Account other)) return false;combines the type check and cast in one line. - String.intern() is rarely needed. The pool saves memory for repeated literals, but manually interning strings from external sources can leak memory if the strings are unique.
Visual Overview
== operator:
primitives → compares VALUES → correct
objects → compares REFERENCES → usually wrong for value check
.equals() method:
Object default → same as == (reference) → useless
String override → compares characters → correct
Integer override → compares int value → correct
BigDecimal override → compares value AND scale → tricky
Decision:
primitive? → ==
null check? → ==
enum? → == (preferred)
String? → .equals()
BigDecimal? → compareTo() == 0
custom object? → override .equals() + .hashCode()
array? → Arrays.equals()
nullable? → Objects.equals()
Key Takeaway
The difference between == and .equals() is identity versus content. For primitives, == compares the actual values and is the only option. For objects, == compares memory addresses – it asks "are these the same object?" while .equals() asks "do these objects represent the same value?"
Two separate String objects containing "SAV2001" are == false (different objects) but .equals() true (same characters). String literals accidentally work with == because they are interned into a shared pool, but strings from databases, network calls, and user input are not interned. Always use .equals().
For BigDecimal, even .equals() is not enough for money comparison because it is scale-sensitive. Use compareTo() == 0 instead. For enums, == is correct and preferred because each constant is a singleton. For arrays, use Arrays.equals() because == on arrays compares references. For null-safe comparison, use Objects.equals().
When you create domain classes, override both equals() and hashCode() together based on the business key. HashMap and HashSet depend on this contract. Records auto-generate both, which is ideal for DTOs and value objects.
One sentence to remember: == compares identity for objects and value for primitives; .equals() compares content – always use .equals() for objects, compareTo for BigDecimal, and override both equals and hashCode in your domain classes.
---