☕ Java

equals() vs ==

The == operator and the equals() method serve fundamentally different purposes in Java. == compares references for objects — it returns true only if both sides point to the exact same object in memory. equals() compares content — it returns true if the two objects represent the same value, regardless of whether they are the same physical object. For primitives, == compares values directly since primitives are not references. Every Java developer must understand this distinction deeply because using == where equals() is needed produces bugs that are difficult to diagnose.

How == Works — Reference Identity

The == operator has exactly one meaning for reference types in Java: it returns true if and only if both sides of the operator hold references to the exact same object in memory. It is asking "are these two variables pointing at the same physical object?" — not "do these two objects contain the same data?" Every object in Java has a unique identity — its memory address. When you create two separate objects with identical content, they are distinct identities. The == operator compares identities. Two variables are == only if they are aliases — they point to the same memory location. For primitive types (int, double, boolean, char, etc.), variables hold the value itself, not a reference to it. So == on primitives correctly compares values: 5 == 5 is true, 5 == 6 is false. The confusion arises specifically with reference types — objects — where the variable holds an address, not the value. Autoboxing makes this particularly tricky. When an int is autoboxed to an Integer, == on the Integer objects compares references. Java caches Integer objects for values between -128 and 127, so Integer.valueOf(100) == Integer.valueOf(100) returns true (same cached object), but Integer.valueOf(200) == Integer.valueOf(200) returns false (different objects because 200 is outside the cache range). This is one of the most surprising behaviours in Java and a source of real production bugs when developers use == on Integer objects.
Java
// ── == on primitives — compares values directly: ─────────────────────
int x = 5;
int y = 5;
int z = 6;
System.out.println(x == y);  // true  — same value
System.out.println(x == z);  // false — different values

// ── == on objects — compares references (memory addresses): ───────────
String a = new String("Hello");
String b = new String("Hello");
String c = a;                    // c and a point to the SAME object

System.out.println(a == b);   // false — different objects, same content
System.out.println(a == c);   // true  — same object (same address)
System.out.println(b == c);   // false — b and a are different objects

// ── == with object values — only true if same instance: ───────────────
Object obj1 = new Object();
Object obj2 = new Object();
Object obj3 = obj1;           // alias

System.out.println(obj1 == obj2);  // false — different instances
System.out.println(obj1 == obj3);  // true  — same instance

// ── Autoboxing cache trap — Integer -128 to 127: ──────────────────────
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1 == i2);   // true  — cached, same object

Integer i3 = 200;
Integer i4 = 200;
System.out.println(i3 == i4);   // false — outside cache, different objects!
System.out.println(i3.equals(i4)); // true  — same value

// ── Always use equals() for wrapper types: ────────────────────────────
Integer a1 = Integer.valueOf(200);
Integer b1 = Integer.valueOf(200);
System.out.println(a1.equals(b1));   // true  — correct value comparison

How equals() Works — Logical Equality

The equals() method is designed to compare the logical content — the value — of two objects. Object.equals() is defined to use == by default (reference identity), but it is designed to be overridden by classes that have a meaningful notion of value equality. String, Integer, Double, Date, and virtually every value class in the Java standard library overrides equals() to compare content. When you call a.equals(b), you are asking "does this object consider b to be equal to itself in terms of its value?" The answer depends entirely on how equals() is implemented in a's class. For String, it means character-by-character comparison. For Integer, it means same int value. For LocalDate, it means same year, month, and day. For a custom class you write, it means whatever you implement — typically comparing the fields that define the object's logical identity. The null safety of equals() is guaranteed by contract: x.equals(null) must always return false, never throw NullPointerException. This means calling equals() on a non-null object with null as the argument is always safe. What is not safe is calling equals() on a null variable — that throws NullPointerException because you are calling a method on null. The defensive idiom is to place known-non-null values (literals, constants, static values) on the left side of the equals() call. The equals() contract requires five properties: reflexivity, symmetry, transitivity, consistency, and null safety. Violating any of these produces buggy behaviour in collections. A broken equals() that is not symmetric (a.equals(b) returns true but b.equals(a) returns false) causes collections to behave unpredictably depending on the order of comparison.
Java
// ── Object.equals() default — reference identity: ────────────────────
class Unoverridden {
    int value;
    Unoverridden(int v) { this.value = v; }
    // No equals() override — uses Object.equals() which uses ==
}

Unoverridden u1 = new Unoverridden(5);
Unoverridden u2 = new Unoverridden(5);
System.out.println(u1.equals(u2));  // false — different objects
System.out.println(u1.equals(u1));  // true  — same object

// ── String overrides equals() — content comparison: ───────────────────
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1.equals(s2));  // true  — same content

// ── Integer overrides equals() — same int value: ─────────────────────
Integer n1 = new Integer(42);
Integer n2 = new Integer(42);
System.out.println(n1.equals(n2));  // true  — same value
System.out.println(n1 == n2);       // false — different objects

// ── Custom equals() implementation: ──────────────────────────────────
public class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) { this.x = x; this.y = y; }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;                          // reflexivity
        if (obj == null || getClass() != obj.getClass()) return false;
        Point other = (Point) obj;
        return Double.compare(x, other.x) == 0
            && Double.compare(y, other.y) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);   // must match equals()
    }
}

Point p1 = new Point(3.0, 4.0);
Point p2 = new Point(3.0, 4.0);
Point p3 = new Point(1.0, 2.0);

System.out.println(p1.equals(p2));  // true  — same coordinates
System.out.println(p1.equals(p3));  // false — different coordinates
System.out.println(p1.equals(null));// falsenull safe

// ── null safety — put non-null on the left: ───────────────────────────
String userInput = null;
// userInput.equals("admin")     — NullPointerException!
"admin".equals(userInput);       // false — safe, no NPE
Objects.equals(userInput, "admin"); // false — safe, handles null on either side

When to Use == and When to Use equals()

The rule is simple but must be applied consistently: use == for primitives and for explicitly checking reference identity; use equals() for all content comparisons of objects. Reference identity checks with == are appropriate in a small set of specific scenarios. Checking if a variable is null (if (obj == null)) always uses == because null is not an object and has no equals() method. Comparing enum constants with == is correct and preferred over equals() because Java guarantees each enum constant is a single unique instance — there is exactly one NORTH value of Direction.NORTH, so == correctly identifies it. Checking whether two variables are aliases (point to the same object) is a legitimate identity check. The JVM also uses == internally for identity checks in identity-sensitive operations like synchronisation on a monitor. For all other comparisons — String, Integer, Double, Long, Boolean, and any custom class — use equals(). The habit of always using equals() for object comparison is a good defensive practice even in cases where == might accidentally work (like string literals in the pool), because it remains correct regardless of how the object was created. The Objects.equals(a, b) utility method is the safest form when either argument might be null. It returns true if both are null, false if exactly one is null, and a.equals(b) otherwise. This eliminates the need to choose which side to put the non-null value on and handles the double-null case in a single clean expression.
Java
// ── When to use == (reference identity): ─────────────────────────────

// 1. Null checks — always use ==:
String s = getNullableString();
if (s == null) {
    System.out.println("String is null");
}
if (s != null) {
    System.out.println(s.length());
}

// 2. Enum comparisons — == is correct and preferred:
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
Day today = Day.WED;
if (today == Day.WED) {                    // ✓ correct
    System.out.println("Midweek");
}
if (today.equals(Day.WED)) {               // also works, but == preferred

}

// 3. Checking aliases (same instance):
List<String> list1 = new ArrayList<>();
List<String> list2 = list1;
if (list1 == list2) {
    System.out.println("Same list object");
}

// ── When to use equals() (content comparison): ────────────────────────

// Strings — always use equals():
String input = getUserInput();
if ("admin".equals(input)) {
    grantAccess();
}

// Wrapper types:
Integer count = getCount();
if (count != null && count.equals(0)) {
    System.out.println("Empty");
}

// Custom objects:
Customer c1 = new Customer("Alice", "alice@x.com");
Customer c2 = new Customer("Alice", "alice@x.com");
System.out.println(c1.equals(c2));   // depends on Customer.equals() implementation

// ── Objects.equals() — safe for possibly null references: ─────────────
String a = null;
String b = "hello";
String c = null;

System.out.println(Objects.equals(a, b));  // falsenull vs "hello"
System.out.println(Objects.equals(a, c));  // true  — both null
System.out.println(Objects.equals(b, "hello")); // true — same content

// ── Comparison summary: ───────────────────────────────────────────────
//
// Situation                    Use
// ──────────────────────────── ─────────────────────────────
// Primitives (int, double...)  ==
// Null check                   == null or != null
// Enum constants               ==
// String content               .equals() or "literal".equals(var)
// Integer, Long, Double...     .equals()
// Custom objects               .equals() (if correctly overridden)
// Either side might be null    Objects.equals(a, b)
// Same object check            ==

Related Topics in Strings

String Class
String is one of the most fundamental classes in Java — used in virtually every program, yet deeply misunderstood by many developers. A String represents an immutable sequence of Unicode characters. It is not a primitive type but a full class in java.lang, automatically imported into every Java file. Understanding String means understanding how it is stored in memory, why it is immutable, how the string pool works, what the difference between == and equals() means for strings, and how to use the class efficiently. This entry covers String's nature as a class, its internal representation, the critical distinction between reference equality and value equality, String's place in the type hierarchy, and the design decisions that make String behave the way it does.
String Pool
The string pool (also called the string intern pool or string constant pool) is a special memory region maintained by the JVM that stores a single copy of each unique string value. When two string literals have the same content, they refer to the same object in the pool rather than two separate objects. The pool is a flyweight pattern applied at the language level — it dramatically reduces memory consumption in applications that use many repeated string values, which is nearly every application. This entry covers how the pool works, where it lives in JVM memory, how to interact with it programmatically, the intern() method, performance implications, and when to use or avoid pool entries.
Immutable String
String immutability is the most important design decision in Java's String class. Once a String object is created, its character sequence can never change. No method on String modifies the string; every method that appears to modify returns a new String object containing the result. This design decision drives thread safety, enables the string pool, makes strings safe hash map keys, and simplifies reasoning about string values. Understanding why String is immutable, how immutability is enforced, and what the consequences of immutability are clarifies the behaviour of virtually every piece of Java code that handles strings.
Mutable String
Java provides two mutable string classes for scenarios where String's immutability would be inefficient: StringBuilder and StringBuffer. Both maintain an internal character buffer that can be modified in place — characters can be appended, inserted, deleted, and replaced without creating new objects. StringBuilder is the modern choice for single-threaded use; StringBuffer is the legacy thread-safe version with synchronised methods. This entry covers the internal buffer mechanics, the full API of both classes, performance characteristics, when to use each, thread safety implications, and the complete patterns for efficient string construction.