Alexander Claes
← Learning Java

Identity and Equality

Same object or just the same value? Learn how Java identity and equality differ, when to use == or equals, and why wrappers, null, arrays, and hashCode need care.

Alexander Claes Alexander Claes 8 min read
Learning Java
Two references connect to a single shared object, beside two separate objects containing matching values.

Two objects can represent the same value without being the same object. That distinction is the difference between equality and identity—and it explains why comparing Java objects with == can produce surprising results.

In the wrapper-class article, we saw that an Integer represents an int value as an object. Now let’s look more closely at what it means for two of those objects to be “the same”.

Two different questions

Imagine two copies of the same book. They have the same text, but they are separate physical books. Equality might ask whether their contents match. Identity asks whether you are holding the exact same copy.

In Java, == between object references asks the identity question. The equals method asks the equality question according to the rules defined by that class. It does not automatically compare every field.

Start with primitives

int first = 42;
int second = 42;
System.out.println(first == second); // true

Here, both operands are primitives, so Java compares their numeric values. There are no object identities to compare. This is why “never use ==” is not a useful rule: the meaning depends on the types involved.

One object, two references

An object variable holds a reference, not the object itself. Assigning or passing that reference does not copy the entire object and its contents. This avoids the work of making a full copy, but it also means that different parts of a program can share access to the same object.

Object first = new Object();
Object second = first;
Object third = new Object();

System.out.println(first == second); // true
System.out.println(first == third);  // false

The assignment to second copies the reference, not the object. First and second therefore refer to the same object. Third refers to a different object, created by a separate new expression.

Reference identity is not a comparison of variable names or a user-visible memory address. The question is simply whether both references identify the same object. Two null references also compare equal with ==.

Shared references: aliasing and method calls

When two variables refer to the same object, they are aliases. If that object is mutable—meaning its state can change—a change through one reference is visible through the other. For example, StringBuilder is a mutable object for building text; unlike String, its contents can be edited.

StringBuilder first = new StringBuilder("Hello");
StringBuilder second = first;
second.append(" Java");

System.out.println(first);           // Hello Java
System.out.println(first == second); // true

The object has changed, but its identity has not. Both variables still refer to the same StringBuilder. Aliasing is useful when sharing changes is intentional, but surprising when you thought you had an independent copy.

The same sharing happens when you pass an object to a method. Java passes arguments by value: the parameter gets a copy of the reference, not a copy of the object.

static void change(StringBuilder text) {
    text.append(" Java");              // Changes the shared object
    text = new StringBuilder("Other"); // Reassigns only the parameter
}

// At the call site:
StringBuilder message = new StringBuilder("Hello");
change(message);
System.out.println(message); // Hello Java

The method can modify the original object through its copied reference. Reassigning text, however, does not reassign message. This is the distinction between changing an object and changing which object a variable refers to.

A final reference does not freeze the object

Declaring a reference variable final prevents reassignment after it has been initialized. It does not make the referenced object immutable.

final StringBuilder message = new StringBuilder("Hello");
message.append(" Java"); // Allowed: modify the existing object

// message = new StringBuilder("Other"); // Does not compile

Here, the reference must keep pointing to the same StringBuilder, but the builder’s contents can change. Other aliases can also change that same object. Immutability comes from the object’s design, not from putting final on a variable. String and the primitive wrapper classes are immutable; StringBuilder is not.

Strings: same text, different objects

String first = new String("Java");
String second = new String("Java");

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true

We deliberately use new String here to create separate objects for the demonstration; ordinary code usually just uses string literals. String overrides equals to compare character sequences, so these two objects are equal even though their identities differ.

String literals introduce another twist: Java shares interned literals with the same contents.

String first = "Java";
String second = "Java";
System.out.println(first == second); // true: the shared literal

That result does not make == a text comparison. Text obtained from input or other runtime operations need not share that object. Use equals for matching text, as described in the String API.

Why Integer comparisons can be misleading

Integer first = 42;
Integer second = 42;

System.out.println(first == second);      // true for these boxed constants
System.out.println(first.equals(second)); // true: matching int values

Java reuses certain boxed values. In particular, boxing these small integer constants shares an instance, so an identity comparison happens to succeed. Do not generalize that behavior to all numbers.

Integer first = Integer.valueOf(1000);
Integer second = Integer.valueOf(1000);

System.out.println(first == second);      // Do not rely on this result
System.out.println(first.equals(second)); // true

Integer.valueOf guarantees caching from -128 through 127 and may cache more. Equality remains the right question when you care about the represented number, regardless of whether instances are reused. See the Integer API.

Types still matter: Integer.valueOf(42).equals(Long.valueOf(42L)) is false. Integer.equals requires another Integer with the same int value, not merely a numerically equivalent object of any type.

Mixing primitives and wrappers changes the comparison

Integer boxed = 42;
int primitive = 42;
System.out.println(boxed == primitive); // true: boxed is unboxed

Here Java unboxes the Integer and compares primitive numbers. This differs from comparing two Integer references. If boxed is null, that unboxing throws NullPointerException. Check the operand types before interpreting what == does.

Comparing safely when null is possible

A reference can also be null, meaning it refers to no object. Storing null is legal, and checking reference == null is safe. Calling an instance method through that reference, however, throws NullPointerException because there is no object to act on. See the article on null and NullPointerException for more examples.

import java.util.Objects;

String first = null;
String second = "Java";

System.out.println(Objects.equals(first, second)); // false
System.out.println(Objects.equals(null, null));   // true
System.out.println(Objects.equals("Java", "Java")); // true

Objects.equals handles null references and otherwise uses the object’s equals implementation. It does not invent a new definition of equality or recursively inspect arbitrary objects. Calling first.equals(second) directly would fail when first is null. The Objects API documents the helper.

What about classes we write ourselves?

Without an override, a class inherits Object.equals, which compares identity. Two instances with matching fields are not automatically equal. We must decide what equality means for the class.

For a small value object representing a two-dimensional point, a reasonable rule is that both coordinates match. Here is a complete implementation:

public final class Point {
    private final int x;
    private final int y;

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

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Point)) {
            return false;
        }
        Point point = (Point) other;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(x, y);
    }
}

@Override asks the compiler to check that we are replacing an inherited method. The parameter must be Object to override this equals method. The instanceof check rejects other types and null; the cast then lets us access the other Point’s coordinates. The class is final, so subclasses cannot introduce conflicting equality rules.

Point first = new Point(2, 3);
Point second = new Point(2, 3);

System.out.println(first == second);      // false
System.out.println(first.equals(second)); // true

Why hashCode belongs next to equals

A collection is an object that groups elements together. A HashSet stores unique elements, while a HashMap associates keys with values. Both use hash codes to narrow their search and equality to distinguish matching values.

The key rule is: equal objects must have equal hash codes. Matching hash codes alone do not prove equality; different values can collide. Override hashCode whenever you define value-based equals.

Equality should agree in both directions, remain consistent while the compared data is unchanged, and be transitive: if a equals b and b equals c, a must equal c. A non-null object must not equal null. These requirements are part of the Object contract.

As a design precaution, avoid changing fields used by equality while an object is stored as a hash-map key or hash-set element. Otherwise a later lookup can search using a different hash from the one used at insertion. Our Point avoids this issue by keeping its coordinates final.

Arrays need their own content comparison

import java.util.Arrays;

int[] first = {1, 2, 3};
int[] second = {1, 2, 3};

System.out.println(first == second);            // false
System.out.println(first.equals(second));       // false
System.out.println(Arrays.equals(first, second)); // true

Arrays do not override equals to compare their contents. For these one-dimensional primitive arrays, use Arrays.equals. For nested arrays, Arrays.deepEquals can compare nested contents. This is another reason not to assume that every equals method means “same fields”.

The question to ask before comparing

Do you want to know whether two references lead to one object, or whether two objects represent equal values? Use == for the first question and the appropriate equality method for the second.

For primitive values, == compares values. For text, use String.equals. For possibly null references, Objects.equals is useful. For your own classes, define equality deliberately and keep hashCode consistent. Identity and equality are both useful—provided you ask the one you actually mean.