Java Tutorial
🔍

Java equals() vs == Operator

Java equals() vs == Operator

This is the most commonly asked Java interview question for freshers — and also the most commonly misunderstood concept in daily Java development. Dozens of production bugs trace back to someone writing == where they needed equals(), or calling equals() on a null reference where Objects.equals() was the safe choice.

The confusion is understandable. For int, double, and other primitives, == compares values and works exactly as expected. For objects — including String, Integer, and every custom class — == compares memory addresses, not content. Two objects that hold identical data can return false for == simply because they are stored at different locations in memory.

The Core Rule — One Sentence

== compares what is stored in the variable. For primitives, that is the value itself. For objects, that is the memory address of the object (the reference).

Primitive variable:             Object variable:

  int x = 5;                    String s = "hello";
  ┌───────┐                     ┌───────────┐
  │   5   │  ← x stores value   │  address  │  ← s stores address
  └───────┘                     └─────┬─────┘
                                      │
                                      ▼
                               ┌─────────────┐
                               │  "hello"    │  ← actual String object
                               └─────────────┘

  x == 5         → compares 5 == 5 → true
  s == "hello"   → compares address == pool address
                  (may be true for literals, false for new String())

== With Primitives — Works as Expected

For all eight primitive types — int, long, double, float, boolean, char, byte, short== compares the actual stored values directly. This works exactly as most beginners expect.

1// File: PrimitiveEquality.java 2 3public class PrimitiveEquality { 4 5 public static void main(String[] args) { 6 7 // int comparison 8 int a = 100; 9 int b = 100; 10 int c = 200; 11 System.out.println("=== int ==="); 12 System.out.println("a == b : " + (a == b)); // true — both store 100 13 System.out.println("a == c : " + (a == c)); // false — 100 != 200 14 15 // double comparison 16 double price1 = 99.99; 17 double price2 = 99.99; 18 System.out.println("\n=== double ==="); 19 System.out.println("price1 == price2 : " + (price1 == price2)); // true 20 21 // boolean comparison 22 boolean isActive = true; 23 boolean isPending = false; 24 System.out.println("\n=== boolean ==="); 25 System.out.println("isActive == true : " + (isActive == true)); // true 26 System.out.println("isPending == isActive: " + (isPending == isActive)); // false 27 28 // char comparison — compares Unicode values 29 char grade1 = 'A'; 30 char grade2 = 'A'; 31 char grade3 = 'B'; 32 System.out.println("\n=== char ==="); 33 System.out.println("grade1 == grade2 : " + (grade1 == grade2)); // true — same Unicode 34 System.out.println("grade1 == grade3 : " + (grade1 == grade3)); // false 35 36 // Floating-point precision warning 37 double x = 0.1 + 0.2; 38 double y = 0.3; 39 System.out.println("\n=== Floating-point precision ==="); 40 System.out.println("0.1 + 0.2 == 0.3 : " + (x == y)); // false — precision issue! 41 System.out.println("0.1 + 0.2 = " + x); // 0.30000000000000004 42 System.out.println("Use Math.abs for double comparison: " 43 + (Math.abs(x - y) < 0.0001)); // true — safe comparison 44 } 45}
Output:
=== int ===
a == b : true
a == c : false

=== double ===
price1 == price2 : true

=== boolean ===
isActive == true     : true
isPending == isActive: false

=== char ===
grade1 == grade2 : true
grade1 == grade3 : false

=== Floating-point precision ===
0.1 + 0.2 == 0.3 : false
0.1 + 0.2 = 0.30000000000000004
Use Math.abs for double comparison: true

One important caveat: never use == to compare double or float values that result from arithmetic. Floating-point arithmetic introduces tiny precision errors — 0.1 + 0.2 is not exactly 0.3 in binary floating-point. Use Math.abs(a - b) < epsilon for numeric tolerance comparison.

== With Objects — Compares References, Not Content

For any object type — String, Integer, ArrayList, or a custom class — == compares memory addresses. Two different objects with identical content return false.

1// File: ObjectEquality.java 2 3public class ObjectEquality { 4 5 static class Product { 6 String name; 7 double price; 8 9 Product(String name, double price) { 10 this.name = name; 11 this.price = price; 12 } 13 } 14 15 public static void main(String[] args) { 16 17 Product p1 = new Product("Laptop", 45000.0); 18 Product p2 = new Product("Laptop", 45000.0); // identical data 19 Product p3 = p1; // same reference 20 21 System.out.println("=== Object Reference Check ==="); 22 System.out.println("p1 == p2 : " + (p1 == p2)); // false — two different objects 23 System.out.println("p1 == p3 : " + (p1 == p3)); // true — same object 24 System.out.println("p2 == p3 : " + (p2 == p3)); // false 25 26 System.out.println(); 27 28 // Identity hash code confirms different objects 29 System.out.println("p1 address: " + System.identityHashCode(p1)); 30 System.out.println("p2 address: " + System.identityHashCode(p2)); // different 31 System.out.println("p3 address: " + System.identityHashCode(p3)); // same as p1 32 33 System.out.println(); 34 35 // What == actually means for objects 36 System.out.println("=== What == means for objects ==="); 37 System.out.println("p1 == p2 means: do p1 and p2 point to the same object?"); 38 System.out.println("Answer: " + (p1 == p2)); // false — they are different objects 39 System.out.println(); 40 System.out.println("p1.name.equals(p2.name): " + p1.name.equals(p2.name)); // true 41 System.out.println("p1.price == p2.price : " + (p1.price == p2.price)); // true (primitive) 42 } 43}
Output:
=== Object Reference Check ===
p1 == p2 : false
p2 == p3 : false
p1 == p3 : true

p1 address: 1173230247
p2 address: 856419764
p3 address: 1173230247

=== What == means for objects ===
p1 == p2  means: do p1 and p2 point to the same object?
Answer: false

p1.name.equals(p2.name): true
p1.price == p2.price   : true (primitive)

p3 = p1 does not copy the object — it copies the reference (memory address). Both p1 and p3 now point to the same Product object. p2 is a separate object with the same field values. The identity hash codes confirm this.

== With Strings — The Tricky Case

Strings behave in a way that confuses beginners most: == sometimes returns true and sometimes false for strings with identical content — depending on how the string was created.

1// File: StringEquality.java 2 3public class StringEquality { 4 5 public static void main(String[] args) { 6 7 // Case 1 — String literals — stored in String Pool — same object reused 8 String s1 = "hello"; 9 String s2 = "hello"; 10 11 System.out.println("=== String Literals (Pool) ==="); 12 System.out.println("s1 == s2 : " + (s1 == s2)); // true — same pool object 13 System.out.println("s1.equals(s2) : " + s1.equals(s2)); // true 14 15 System.out.println(); 16 17 // Case 2 — new String() — always creates a new heap object 18 String s3 = new String("hello"); 19 String s4 = new String("hello"); 20 21 System.out.println("=== new String() (Heap) ==="); 22 System.out.println("s3 == s4 : " + (s3 == s4)); // false — different objects 23 System.out.println("s3.equals(s4) : " + s3.equals(s4)); // true — same content 24 System.out.println("s1 == s3 : " + (s1 == s3)); // false — pool vs heap 25 26 System.out.println(); 27 28 // Case 3 — runtime-built strings — always new heap objects 29 String base = "hel"; 30 String concat = base + "lo"; // built at runtime — new heap object 31 32 System.out.println("=== Runtime Concatenation (Heap) ==="); 33 System.out.println("s1 == concat : " + (s1 == concat)); // false — concat is on heap 34 System.out.println("s1.equals(concat): " + s1.equals(concat)); // true — same content 35 36 System.out.println(); 37 38 // The lesson — NEVER use == for string content comparison 39 // Strings from database, user input, API — always heap objects 40 System.out.println("=== The Real-World Problem ==="); 41 String fromDB = fetchFromDatabase(); // always a new heap object 42 String expected = "active"; 43 44 System.out.println("fromDB == expected : " + (fromDB == expected)); // false — bug! 45 System.out.println("expected.equals(fromDB): " + expected.equals(fromDB)); // true — correct 46 } 47 48 // Simulates reading a String from a database or API response 49 static String fetchFromDatabase() { 50 return new String("active"); // always a new heap object in reality 51 } 52}
Output:
=== String Literals (Pool) ===
s1 == s2       : true
s1.equals(s2)  : true

=== new String() (Heap) ===
s3 == s4       : false
s3.equals(s4)  : true
s1 == s3       : false

=== Runtime Concatenation (Heap) ===
s1 == concat   : false
s1.equals(concat): true

=== The Real-World Problem ===
fromDB == expected     : false
fromDB.equals(fromDB)  : true

This is exactly why == for Strings fails in production: values from databases, files, APIs, and user input are always new heap objects — they are never in the String pool. equals() works correctly for all of them because it compares content, not address.

== With Wrapper Classes — The Integer Cache Trap

Wrapper classes (Integer, Long, Double) are objects. == compares their references. However, Java caches Integer values from -128 to 127 in a special pool — similar to the String pool — which makes == return true for small integers and false for larger ones. This is one of the most famous Java interview traps.

1// File: WrapperEquality.java 2 3public class WrapperEquality { 4 5 public static void main(String[] args) { 6 7 // Integer cache — -128 to 127 are cached 8 Integer a = 100; 9 Integer b = 100; 10 Integer c = 200; 11 Integer d = 200; 12 13 System.out.println("=== Integer Cache Trap ==="); 14 System.out.println("a == b (value 100): " + (a == b)); // true — cached range! 15 System.out.println("c == d (value 200): " + (c == d)); // false — outside cache 16 System.out.println(); 17 System.out.println("a.equals(b) : " + a.equals(b)); // true — always reliable 18 System.out.println("c.equals(d) : " + c.equals(d)); // true — always reliable 19 20 System.out.println(); 21 22 // Why the cache exists — performance optimisation 23 // Autoboxing frequently used small integers reuses the same objects 24 System.out.println("=== Why This Happens ==="); 25 System.out.println("Integer.valueOf(100) == Integer.valueOf(100): " 26 + (Integer.valueOf(100) == Integer.valueOf(100))); // true — cached 27 System.out.println("Integer.valueOf(200) == Integer.valueOf(200): " 28 + (Integer.valueOf(200) == Integer.valueOf(200))); // false — not cached 29 30 System.out.println(); 31 32 // Same trap with Long 33 Long x = 50L; 34 Long y = 50L; 35 Long p = 200L; 36 Long q = 200L; 37 38 System.out.println("=== Long Cache ==="); 39 System.out.println("50L == 50L : " + (x == y)); // true — in cache range 40 System.out.println("200L == 200L : " + (p == q)); // false — outside cache 41 42 System.out.println(); 43 44 // Safe comparison — always use equals() for wrapper types 45 System.out.println("=== Always Use equals() for Wrappers ==="); 46 Integer score1 = 500; 47 Integer score2 = 500; 48 System.out.println("score1 == score2 : " + (score1 == score2)); // false — unreliable 49 System.out.println("score1.equals(score2) : " + score1.equals(score2)); // true — reliable 50 51 // Unboxing — comparing wrapper to primitive uses == safely 52 int primitive = 500; 53 System.out.println("score1 == primitive : " + (score1 == primitive)); // true — auto-unboxed 54 } 55}
Output:
=== Integer Cache Trap ===
a == b (value 100): true
c == d (value 200): false

a.equals(b)        : true
c.equals(d)        : true

=== Why This Happens ===
Integer.valueOf(100) == Integer.valueOf(100): true
Integer.valueOf(200) == Integer.valueOf(200): false

=== Long Cache ===
50L == 50L   : true
200L == 200L : false

=== Always Use equals() for Wrappers ===
score1 == score2       : false
score1.equals(score2)  : true
score1 == primitive    : true

The Integer cache is why interview code like Integer a = 127; Integer b = 127; System.out.println(a == b) prints true, but Integer a = 128; Integer b = 128; prints false. This is a guaranteed interview question — the only safe answer is: never use == for wrapper objects.

equals() — Object Content Comparison

equals() is a method inherited from java.lang.Object. The default implementation in Object behaves exactly like == — reference comparison. To make it compare content, you must override it in your class.

1// File: EqualsOverrideDemo.java 2 3import java.util.Objects; 4 5public class EqualsOverrideDemo { 6 7 // Without override — uses Object.equals() which is the same as == 8 static class ProductWithout { 9 String sku; 10 ProductWithout(String sku) { this.sku = sku; } 11 } 12 13 // With override — compares content correctly 14 static class Product { 15 private final String sku; 16 private final String name; 17 private final double price; 18 19 Product(String sku, String name, double price) { 20 this.sku = sku; 21 this.name = name; 22 this.price = price; 23 } 24 25 @Override 26 public boolean equals(Object obj) { 27 if (this == obj) return true; // Step 1: same reference 28 if (obj == null) return false; // Step 2: null check 29 if (!(obj instanceof Product other)) return false; // Step 3: type check 30 return Objects.equals(sku, other.sku); // Step 4: field comparison 31 // Two Products are "equal" if they have the same SKU 32 } 33 34 @Override 35 public int hashCode() { 36 return Objects.hash(sku); // must match equals — same field 37 } 38 39 @Override 40 public String toString() { 41 return "Product{" + sku + ", " + name + ", Rs." + price + "}"; 42 } 43 } 44 45 public static void main(String[] args) { 46 47 // Without override — equals() behaves like == 48 ProductWithout pw1 = new ProductWithout("SKU-001"); 49 ProductWithout pw2 = new ProductWithout("SKU-001"); 50 51 System.out.println("=== Without Override ==="); 52 System.out.println("pw1 == pw2 : " + (pw1 == pw2)); // false 53 System.out.println("pw1.equals(pw2) : " + pw1.equals(pw2)); // false — same as == 54 55 System.out.println(); 56 57 // With override — equals() compares SKU content 58 Product p1 = new Product("SKU-001", "Laptop", 45000.0); 59 Product p2 = new Product("SKU-001", "Laptop", 45000.0); // same SKU 60 Product p3 = new Product("SKU-002", "Monitor", 18000.0); // different SKU 61 62 System.out.println("=== With Override ==="); 63 System.out.println("p1 == p2 : " + (p1 == p2)); // false — different objects 64 System.out.println("p1.equals(p2) : " + p1.equals(p2)); // true — same SKU 65 System.out.println("p1.equals(p3) : " + p1.equals(p3)); // false — different SKU 66 System.out.println("p1.equals(null): " + p1.equals(null)); // false — null safe 67 68 System.out.println(); 69 70 // HashSet deduplication depends on correct equals() + hashCode() 71 java.util.Set<Product> catalogue = new java.util.HashSet<>(); 72 catalogue.add(p1); 73 catalogue.add(p2); // same SKU as p1 — should not be added 74 catalogue.add(p3); 75 76 System.out.println("Unique products in catalogue: " + catalogue.size()); // 2 77 } 78}
Output:
=== Without Override ===
pw1 == pw2       : false
pw1.equals(pw2)  : false

=== With Override ===
p1 == p2       : false
p1.equals(p2)  : true
p1.equals(p3)  : false
p1.equals(null): false

Unique products in catalogue: 2

HashSet uses equals() and hashCode() together. Without overriding both, HashSet treats every object as unique regardless of content — which silently allows duplicates in a catalogue that should contain one entry per SKU.

== vs equals() — Complete Comparison Table

Aspect==equals()
TypeLanguage operatorMethod from java.lang.Object
For primitivesCompares values directlyNot applicable — primitives have no methods
For objectsCompares memory addresses (references)Compares logical content — depends on override
Default object behaviourReference equalitySame as == if not overridden in the class
Can be overriddenNo — operator is fixedYes — every class can define its own logic
Null safetynull == null is trueThrows NullPointerException if called on null
String literalsMay return true (pool reuse)Always returns true for equal content
Strings from DB / APIReturns false (heap objects)Returns true for equal content
Integer values 127 or belowReturns true (cached)Always returns true for equal values
Integer values above 127Returns false (heap)Always returns true for equal values
Used in HashSet / HashMapNever — collections use equals()Always — alongside hashCode()
When to useReference identity, null checks, primitivesContent equality for objects
Reliable for contentNo — unpredictable for objectsYes — always reliable when correctly overridden

Null Handling — equals() vs == vs Objects.equals()

1// File: NullHandlingDemo.java 2 3import java.util.Objects; 4 5public class NullHandlingDemo { 6 7 public static void main(String[] args) { 8 9 String a = "hello"; 10 String b = null; 11 String c = null; 12 13 // == with null — always safe 14 System.out.println("=== == with null ==="); 15 System.out.println("b == null : " + (b == null)); // true 16 System.out.println("a == null : " + (a == null)); // false 17 System.out.println("b == c : " + (b == c)); // true — both null 18 19 System.out.println(); 20 21 // equals() with null — NPE if called on null reference 22 System.out.println("=== equals() with null ==="); 23 try { 24 System.out.println(b.equals("hello")); // NPE — b is null 25 } catch (NullPointerException e) { 26 System.out.println("b.equals('hello') → NullPointerException!"); 27 } 28 29 // Safe pattern — constant on left 30 System.out.println("'hello'.equals(b): " + "hello".equals(b)); // false — no NPE 31 32 System.out.println(); 33 34 // Objects.equals() — null-safe on BOTH sides 35 System.out.println("=== Objects.equals() — null-safe both sides ==="); 36 System.out.println("Objects.equals(a, 'hello'): " + Objects.equals(a, "hello")); // true 37 System.out.println("Objects.equals(b, 'hello'): " + Objects.equals(b, "hello")); // false 38 System.out.println("Objects.equals(b, c) : " + Objects.equals(b, c)); // true — both null 39 System.out.println("Objects.equals(a, null) : " + Objects.equals(a, null)); // false 40 41 System.out.println(); 42 43 // When to use each 44 System.out.println("=== Choosing the right tool ==="); 45 System.out.println("Use == null : to check if a reference is null"); 46 System.out.println("Use equals() : when you KNOW left side is non-null"); 47 System.out.println("Use Objects.equals(): when EITHER side could be null"); 48 } 49}
Output:
=== == with null ===
b == null    : true
a == null    : false
b == c       : true

=== equals() with null ===
b.equals('hello') → NullPointerException!
'hello'.equals(b): false

=== Objects.equals() — null-safe both sides ===
Objects.equals(a, 'hello'): true
Objects.equals(b, 'hello'): false
Objects.equals(b, c)      : true
Objects.equals(a, null)   : false

=== Choosing the right tool ===
Use == null        : to check if a reference is null
Use equals()       : when you KNOW left side is non-null
Use Objects.equals(): when EITHER side could be null

Real-World Example — E-Commerce Order Validation

The Business Problem

An order management system at a platform like Flipkart or Meesho processes orders through a validation pipeline. Order IDs must match exactly (case-sensitive equals()). Payment status from the payment gateway — which comes as a new String from an API call — must be checked correctly (not with ==). Two Order objects must be checked for equality based on their order ID field. And Integer order counts from a summary map must be compared safely without the Integer cache trap.

1// File: Order.java 2 3import java.util.Objects; 4 5public class Order { 6 7 private final String orderId; 8 private final String customerId; 9 private final double amount; 10 private String paymentStatus; 11 12 public Order(String orderId, String customerId, double amount) { 13 this.orderId = orderId; 14 this.customerId = customerId; 15 this.amount = amount; 16 this.paymentStatus = "PENDING"; 17 } 18 19 public String getOrderId() { return orderId; } 20 public String getCustomerId() { return customerId; } 21 public double getAmount() { return amount; } 22 public String getPaymentStatus() { return paymentStatus; } 23 24 public void setPaymentStatus(String status) { 25 this.paymentStatus = status; 26 } 27 28 // Two orders are equal when they have the same orderId 29 @Override 30 public boolean equals(Object obj) { 31 if (this == obj) return true; 32 if (!(obj instanceof Order other)) return false; 33 return Objects.equals(orderId, other.orderId); 34 } 35 36 @Override 37 public int hashCode() { 38 return Objects.hash(orderId); 39 } 40 41 @Override 42 public String toString() { 43 return "Order{id=" + orderId 44 + ", cust=" + customerId 45 + ", amt=Rs." + amount 46 + ", status=" + paymentStatus + "}"; 47 } 48}
1// File: OrderValidator.java 2 3import java.util.Objects; 4 5public class OrderValidator { 6 7 // Validates that an order ID from API matches the expected format 8 // Status comes from payment gateway — always a new heap String 9 public static boolean validatePaymentStatus(String gatewayResponse, 10 String expectedStatus) { 11 // WRONG — gatewayResponse is a new String from API — == will fail 12 // if (gatewayResponse == expectedStatus) { ... } 13 14 // CORRECT — equals() compares content, not address 15 return expectedStatus.equals(gatewayResponse); 16 } 17 18 // Checks whether two Order objects represent the same order 19 public static boolean isSameOrder(Order order1, Order order2) { 20 // == would only be true if they are the literally the same object 21 // equals() checks the orderId field as defined in Order.equals() 22 return Objects.equals(order1, order2); 23 } 24 25 // Checks whether order count meets a threshold 26 // Returns safe comparison avoiding Integer cache issues 27 public static boolean meetsMinimumCount(Integer actual, int required) { 28 if (actual == null) return false; 29 30 // WRONG — == on Integer can fail above 127 31 // if (actual == required) { ... } 32 33 // CORRECT — unbox for comparison with a primitive int 34 return actual >= required; // auto-unboxing — safe for all values 35 } 36}
1// File: OrderSystemDemo.java 2 3import java.util.HashMap; 4import java.util.Map; 5 6public class OrderSystemDemo { 7 8 public static void main(String[] args) { 9 10 System.out.println("╔══════════════════════════════════════════╗"); 11 System.out.println("║ ORDER VALIDATION SYSTEM ║"); 12 System.out.println("╚══════════════════════════════════════════╝\n"); 13 14 // Payment gateway returns a NEW String object — not from pool 15 String gatewayResponse = new String("SUCCESS"); 16 String expected = "SUCCESS"; 17 18 System.out.println("=== Payment Status Validation ==="); 19 System.out.println("Response : " + gatewayResponse); 20 System.out.println("Expected : " + expected); 21 System.out.println("Wrong (==) : " + (gatewayResponse == expected)); // false — bug! 22 System.out.println("Correct (equals): " 23 + OrderValidator.validatePaymentStatus(gatewayResponse, expected)); // true 24 25 System.out.println(); 26 27 // Order equality check 28 Order order1 = new Order("ORD-2024-001", "CUST-501", 1299.50); 29 Order order2 = new Order("ORD-2024-001", "CUST-501", 1299.50); // same order ID 30 Order order3 = new Order("ORD-2024-002", "CUST-502", 499.00); // different ID 31 32 System.out.println("=== Order Equality ==="); 33 System.out.println("order1 == order2 : " + (order1 == order2)); // false 34 System.out.println("order1.equals(order2) : " + order1.equals(order2)); // true 35 System.out.println("isSameOrder(order1, order2) : " 36 + OrderValidator.isSameOrder(order1, order2)); // true 37 System.out.println("isSameOrder(order1, order3) : " 38 + OrderValidator.isSameOrder(order1, order3)); // false 39 40 System.out.println(); 41 42 // Integer comparison in order counts 43 Map<String, Integer> orderCounts = new HashMap<>(); 44 orderCounts.put("CUST-501", 200); // above Integer cache range 45 orderCounts.put("CUST-502", 50); // within Integer cache range 46 47 Integer count1 = orderCounts.get("CUST-501"); 48 Integer count2 = 200; 49 50 System.out.println("=== Integer Count Comparison ==="); 51 System.out.println("count1 value : " + count1); 52 System.out.println("count1 == 200 : " + (count1 == count2)); // false — above 127 53 System.out.println("count1.equals(count2): " + count1.equals(count2)); // true 54 System.out.println("count1 >= 100 : " + OrderValidator.meetsMinimumCount(count1, 100)); // true 55 56 System.out.println(); 57 58 // Full validation pipeline 59 System.out.println("=== Full Pipeline ==="); 60 Order[] orders = {order1, order2, order3}; 61 String[] statuses = {new String("SUCCESS"), new String("FAILED"), new String("SUCCESS")}; 62 63 for (int i = 0; i < orders.length; i++) { 64 boolean valid = OrderValidator.validatePaymentStatus(statuses[i], "SUCCESS"); 65 System.out.printf(" %s | Status: %-8s | Payment: %s%n", 66 orders[i].getOrderId(), 67 statuses[i], 68 valid ? "VALIDATED" : "REJECTED"); 69 } 70 } 71}
Output:
╔══════════════════════════════════════════╗
║       ORDER VALIDATION SYSTEM           ║
╚══════════════════════════════════════════╝

=== Payment Status Validation ===
Response  : SUCCESS
Expected  : SUCCESS
Wrong  (==)     : false
Correct (equals): true

=== Order Equality ===
order1 == order2                : false
order1.equals(order2)           : true
isSameOrder(order1, order2)     : true
isSameOrder(order1, order3)     : false

=== Integer Count Comparison ===
count1 value         : 200
count1 == 200        : false
count1.equals(count2): true
count1 >= 100        : true

=== Full Pipeline ===
  ORD-2024-001 | Status: SUCCESS  | Payment: VALIDATED
  ORD-2024-001 | Status: FAILED   | Payment: REJECTED
  ORD-2024-002 | Status: SUCCESS  | Payment: VALIDATED

Three distinct comparison failures that equals() solves correctly: API response Strings compared with == (would always fail), Order objects compared without equals() override (would always fail), and Integer values above 127 compared with == (would fail intermittently).

Best Practices

Use equals() for all object content comparison — always. This applies to String, Integer, Long, Double, and every custom class. The only exception is primitives (int, double, boolean, char), which have no equals() method and where == is the correct tool.

Put the known non-null constant on the left side of equals(). "SUCCESS".equals(gatewayResponse) is safe when gatewayResponse might be null. gatewayResponse.equals("SUCCESS") throws NullPointerException. This one habit eliminates an entire category of null-related crashes.

Use Objects.equals(a, b) when either side might be null. This utility handles all null combinations without requiring explicit null checks. It is the cleanest option when comparing two values that might both legitimately be null — like optional fields in a database record.

Never use == to compare Integer, Long, or other wrapper objects. The cache range of -128 to 127 makes == unreliable — it returns true for small values and false for larger ones. Always use .equals() or unbox to a primitive first.

Common Mistakes

Mistake 1 — Using == for String Values From Outside the Literal Pool

1// Common in login systems, API validation, configuration checks 2String statusFromDB = getFromDatabase("order_status"); // new heap String 3String expected = "PLACED"; 4 5if (statusFromDB == expected) { // false — heap vs literal 6 processOrder(); // never executes — silent bug 7} 8 9if (expected.equals(statusFromDB)) { // true — correct content comparison 10 processOrder(); // executes correctly 11}

Mistake 2 — Relying on Integer == for Values That Might Exceed 127

1Integer attempts = getLoginAttempts(); // from session — might be Integer(200) 2Integer threshold = 200; 3 4if (attempts == threshold) { // false when value > 127 — intermittent bug 5 lockAccount(); // silently fails for high-value integers 6} 7 8if (attempts.equals(threshold)) { // true — reliable for all values 9 lockAccount(); 10}

Mistake 3 — Calling equals() on a Possibly Null Variable

1String role = getUserRole(); // might return null 2 3if (role.equals("admin")) { // NullPointerException if role is null 4 grantAccess(); 5} 6 7// Safe alternatives: 8if ("admin".equals(role)) { // constant on left — null-safe 9 grantAccess(); 10} 11 12// Or: 13if (Objects.equals(role, "admin")) { // null-safe both sides 14 grantAccess(); 15}

Mistake 4 — Thinking equals() Without Override Compares Content

1class Ticket { 2 String id; 3 Ticket(String id) { this.id = id; } 4 // No equals() override 5} 6 7Ticket t1 = new Ticket("TKT-001"); 8Ticket t2 = new Ticket("TKT-001"); 9 10System.out.println(t1.equals(t2)); // false — Object.equals() is the same as == 11// Without override, equals() checks reference — not id field 12 13// Fix: override equals() in Ticket class using the id field

Interview Questions

Q1. What is the difference between == and equals() in Java?

== compares what is stored in the variable. For primitives, that is the value directly — so 5 == 5 is true. For objects, that is the memory address — so two different objects with the same content return false. equals() is a method that compares logical content. The default implementation in Object uses ==, so it also compares references. When a class overrides equals() — as String, Integer, and well-designed custom classes do — it compares meaningful fields instead. Use == for primitives and null checks. Use equals() for all object content comparison.

Q2. Why does Integer a = 127; Integer b = 127; a == b return true, but the same with 128 returns false?

Java caches Integer objects for values from -128 to 127 in a special pool (similar to the String pool). When you autobox a value in this range, Java reuses the same cached object rather than creating a new one. Both a and b get the same cached object, so == returns true. For values outside this range, Java creates a new Integer object on the heap each time, so a and b point to different objects and == returns false. This is why equals() must always be used for Integer comparison — its result is consistent regardless of value.

Q3. Why does equals() without an override behave the same as ==?

equals() is defined in java.lang.Object, the root of all Java classes. The default implementation simply returns this == obj — reference comparison. Every class inherits this default unless it explicitly overrides equals(). String, Integer, ArrayList, and other standard library classes override it to compare content. Custom classes must also override it if content equality is needed. Without an override, two different objects with identical fields return false from equals().

Q4. When should you use == instead of equals() for objects?

Use == for exactly two scenarios with objects: checking whether a reference is nullif (obj == null) — which is safe and idiomatic; and intentionally checking whether two variables point to the exact same object in memory — used in specific caching or singleton patterns. For all content comparison, use equals(). The rule: == checks identity, equals() checks equivalence.

Q5. What is Objects.equals() and why is it better than equals() in some cases?

Objects.equals(a, b) is a static utility method from java.util.Objects. It handles null on both sides without any explicit null check: if both are null, it returns true; if one is null, it returns false; if neither is null, it calls a.equals(b). This is safer than a.equals(b) when a might be null, and safer than "constant".equals(b) when both variables might be null. Use Objects.equals() when either argument could legitimately be null — like optional fields, database values that might not be set, or method return values that might return null.

Q6. Does equals() handle null arguments safely?

The Object.equals() contract requires that x.equals(null) must return false for any non-null x. Well-implemented classes — including String — satisfy this contract. However, calling equals() on a null reference — nullVariable.equals("something") — always throws NullPointerException. The method must be called on a non-null object. The constant-on-left pattern "something".equals(variable) handles this correctly regardless of whether variable is null.

FAQs

Can == ever be correct for String comparison?

Only in two specific situations: when you intentionally want to check whether two String variables are the same interned object — which is almost never a business requirement — or when comparing a String reference to null using str == null. For any check involving string content, equals() is the correct and only reliable choice.

Why does null == null return true with == but Objects.equals(null, null) also returns true?

== checks memory addresses. Both null references point to nothing — address 0 — so == returns true. Objects.equals(a, b) explicitly handles the both-null case: if both arguments are null, it returns true without calling any method. This means Objects.equals(null, null) is true and Objects.equals(null, "hello") is false — both safely.

Is it ever correct to compare double values with ==?

For exact equality of literal constants — price == 0.0== is technically safe. But comparing results of arithmetic — like 0.1 + 0.2 == 0.3 — is unreliable because floating-point arithmetic introduces tiny precision errors. The result of 0.1 + 0.2 is 0.30000000000000004, not exactly 0.3. Use Math.abs(a - b) < epsilon where epsilon is a small tolerance value like 0.0001.

Do records in Java automatically provide correct equals() behaviour?

Yes. Java 16+ records automatically generate equals(), hashCode(), and toString(). Two record instances are equal when all their component values are equal — the generated equals() compares all declared fields. You can override the generated equals() in the record body if you need different equality semantics.

What does the equals() contract require?

The Java specification requires equals() to be: reflexivex.equals(x) is always true; symmetric — if x.equals(y) then y.equals(x); transitive — if x.equals(y) and y.equals(z) then x.equals(z); consistent — repeated calls return the same result when no fields change; and null-safex.equals(null) must return false, never throw. Violating any of these causes unpredictable behaviour in collections like HashSet and HashMap.

Summary

== stores what the variable holds. For primitives, that is a value — comparison is direct and correct. For objects, that is a memory address — comparison checks identity, not content. equals() is a method that compares logical content when properly overridden, which is what the vast majority of comparisons in application code actually need.

The two rules that prevent almost every comparison bug: use equals() for all object content comparison instead of ==; and put the known non-null value on the left side of equals(), or use Objects.equals() when either side might be null. The Integer cache and String pool are interview topics that become clear once you understand why == is unreliable for objects — the cache is an optimisation that occasionally makes == appear to work, which is more confusing than helpful.

What to Read Next