Skip to content
Ameen Abozaid
PublishedBEG-026-L02

Identity vs equality

  • Lesson 2 of 6
  • 10 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

Two banknotes can be the same note (same serial number) or merely equal in value. Java has the same two questions for objects, and it uses two different tools: the operator == asks about identity, and the method equals asks about equality. Mixing them up produces one of the most famous beginner bugs: comparing strings with ==.

By the end of this lesson you will know what each tool asks, why "bank" == "bank" can be true while new String("bank") == "bank" is false, and how to write comparisons that are correct every time. Earlier lessons told you to compare text with .equals; this is the full explanation.

Why this matters: in backend code, user input, command names, and status values are text. A comparison that works in your test and fails in production is the worst kind of bug.

This lesson builds on Lesson 1 (the default equals is identity) and on references and null from the objects module.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain what == compares for primitives and for object references.
  • Explain what equals asks and what the default version does.
  • Predict == and equals for strings built in different ways.
  • Explain why string literals are shared, and why that makes == misleading.
  • Fix command == "quit" and write the null-safe form.
  • Choose between identity and equality for a class.

Written Explanation

Two questions

QuestionToolBanknote picture
Is this the very same object in memory?==Same serial number: the same note
Do these two objects have the same value?equalsDifferent notes, same value

For primitives such as int and double, == compares the values: 5 == 5 is true. For object references, == compares the references: it is true when both sides point at the same object (or both are null). It never looks inside the objects.

The default equals is identity

From Lesson 1: Object.equals returns true only for the same object. So for a class that does not override it:

java
Account x = new Account("A-1");
Account y = new Account("A-1");
Account z = x;
System.out.println(x == y);       // false: two objects
System.out.println(x.equals(y));  // false: default equals is identity
System.out.println(x == z);       // true: same object
System.out.println(x.equals(z));  // true

z is not a copy: it is a second name for the same object, like two people holding the same note.

String overrides equals

The String class overrides equals to compare the characters. So a.equals(b) is true when both strings have the same text. That is the correct way to compare text. == on two strings still asks the identity question, and the answer depends on how each string was created.

Why == sometimes works on strings

The Java Language Specification says that string literals (and compile-time constant expressions) with the same text refer to the same String object: they are interned. So:

java
String a = "bank";
String b = "bank";
System.out.println(a == b);       // true: both literals share one object

But a string built at run time is a new object:

java
String c = new String("bank");
String suffix = "ank";
String d = "b" + suffix;          // built at run time: suffix is a variable
System.out.println(a == c);       // false
System.out.println(a == d);       // false
System.out.println(a.equals(c));  // true
System.out.println(a.equals(d));  // true

This is why == is dangerous: it appears to work in a quick test with literals and then fails when the text comes from a file, a network request, or a user typing. The rule is simple: never use == to compare the content of strings. (== on strings is legitimate only when you really mean identity, which is almost never.)

The null-safe comparison

If command could be null, then command.equals("quit") throws NullPointerException. Put the literal first:

java
if ("quit".equals(command)) { ... }   // false when command is null, no exception

A literal is never null, so the call is always safe. The java.util.Objects.equals(a, b) helper does the same for two variables: it returns true if both are null, false if only one is, and otherwise a.equals(b).

Why identity still matters

Identity is not a lesser tool. Two variables that point at the same object are aliases: a change made through one is visible through the other, because there is only one object. Asking x == z is how you check whether two variables are aliases, which is sometimes exactly the question (for example, "am I about to compare an object with itself?"). That is why equals implementations begin with if (this == o) return true;: it is a fast and correct shortcut, since an object is always equal to itself.

A quick checklist before you compare

  1. Are both sides primitives? Use ==.
  2. Are they objects that represent values (strings, money, dates)? Use equals, with the known value on the left if the other side may be null.
  3. Do you genuinely mean "the same object"? Then == is right, and it is worth a comment saying so.

Choosing identity or equality for your own class

  • Entities with a life of their own (an open database connection, a thread, a specific bank account object being modified): identity may be right.
  • Values (money, a date, a coordinate, an email address): two objects with the same data are interchangeable, so equality should compare the data. You do that by overriding equals, which Lesson 3 teaches.

Examples

Example 1: strings built in different ways (simple)

java
String a = "bank";
String b = "bank";
String c = new String("bank");
String suffix = "ank";
String d = "b" + suffix;
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a.equals(c));
System.out.println(a == d);
System.out.println(a.equals(d));

Real output:

text
true
false
true
false
true

Same text in all four strings, but only a == b is true, because only those two are the same literal object. Every equals is true.

Example 2: objects with and without equality (practical)

java
Account x = new Account("A-1");
Account y = new Account("A-1");
Account z = x;
System.out.println(x == y);
System.out.println(x.equals(y));
System.out.println(x == z);
System.out.println(x.equals(z));
text
false
false
true
true

x and y carry identical data but are two objects, and Account does not override equals. Before you complain that this is wrong, ask what the business means: are two account objects with the same number the same account? Probably yes, and that decision is yours to code in Lesson 3.

Example 3: a command loop bug (realistic)

A console tool reads a command from the user and exits on quit. The buggy version:

java
String word = new String("quit");   // stands in for text that arrived at run time
if (word == "quit") System.out.println("== matched"); else System.out.println("== missed");
if ("quit".equals(word)) System.out.println("equals matched");
text
== missed
equals matched

With the test string typed as a literal the == version would have printed "matched" and passed your tests. With real input it misses, so the program never quits. The fix is "quit".equals(word), which is also safe if word is null:

java
String command = null;
System.out.println("quit".equals(command));   // false, no exception
command.equals("quit");                         // throws NullPointerException

Backend Insight

A practical rule for code reviews: any == between two non-primitive values deserves a second look. Either it compares against null (x == null, which is correct and common), or it compares enum-like constants you defined yourself, or it is a bug. A reviewer who asks "what do you mean here, same object or same value?" prevents many production defects.

Text comparison is everywhere in a backend: HTTP method names, header values, role names such as "ADMIN", status codes in a database column. All of these arrive as strings built at run time, so == would fail on them even when they look right in the log. Comparing with "ADMIN".equals(role) is the habit that survives production.

Also think about what kind of equality your business needs. Are two Payment objects with the same id the same payment? Are two Money objects with the same amount equal? These questions decide whether duplicates are detected in a collection, whether a cache finds a record, and whether a retry creates a second charge. You will implement the answers in the next lessons.

Common Mistakes

Mistake 1: Comparing strings with ==

  • Incorrect: if (command == "quit")
  • Correct: if ("quit".equals(command))

Mistake 2: Trusting a test that used literals

A test where both strings are literals will pass with ==. Use equals always, so the code is correct whatever the origin of the text.

Mistake 3: Calling equals on a possibly null variable

command.equals("quit") throws NullPointerException when command is null. Put the known value first, or use Objects.equals(a, b).

Mistake 4: Comparing with == and "fixing" it by trial

Some beginners flip between == and equals until the test turns green. Instead, ask which question you mean (same object, or same value) and write the tool that asks it.

Mistake 5: Expecting equals to work on your own class without overriding it

For a class that did not override equals, a.equals(b) is identity. Two objects with identical fields are still "not equal".

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. What does == compare when both sides are int? When both sides are object references?

Show solution A1

A1. For int it compares the numbers. For object references it asks whether both point at the same object (or are both null).

A2. Say true or false for "a" == "a" and "a".equals("a").

Show solution A2

A2. Both are true. The first is true only because string literals with the same text share one object; the second compares text.

A3. Rewrite if (answer == "yes") correctly.

Show solution A3

A3. if ("yes".equals(answer))

Level B: Application

B1. With String s = new String("go");, give the result of s == "go" and s.equals("go"), and explain the difference in one sentence.

Show solution B1

B1. s == "go" is false and s.equals("go") is true. new String(...) creates a separate object, so identity differs, but the characters are the same.

B2. Account p = new Account("A-1"); Account q = p; (no overrides). What are p == q and p.equals(q)? What if q = new Account("A-1")?

Show solution B2

B2. With q = p, both are true (same object). With q = new Account("A-1"), both are false, because == compares identity and the default equals does too.

B3. role may be null. Write a check that is true only when role is the text ADMIN, without ever throwing.

Show solution B3

B3. "ADMIN".equals(role). Or Objects.equals(role, "ADMIN").

Level C: Challenge

C1. A teammate says: "My == string comparison works in all my unit tests, so it is fine." Explain why the tests mislead and give a code line that would make the same comparison fail.

Show solution C1

C1. In tests the strings are usually literals, which Java shares, so == accidentally returns true. In production the text comes from input, so it is a new object. For example String x = new String("quit"); x == "quit" is false even though the text is the same.

C2. Decide for each: identity or equality for a class Money(cents, currency), a class Printer that represents one physical printer, a class EmailAddress(text). Give one sentence of reasoning each.

Show solution C2

C2. Money: equality, 100 SAR is the same value wherever it appears. Printer: identity, each object stands for one physical device, two printers with the same model are still different. EmailAddress: equality, two objects holding the same text are interchangeable.

Mini Challenge

Write a small program with a method boolean isStop(String input) that returns true when input is exactly the text stop and is safe for null. Call it with a literal, with new String("stop"), with null, and with "Stop". Print each result.

Check your work: the first two return true, null returns false without crashing, and "Stop" returns false because equals is case sensitive.

Summary & Next Lesson

Key takeaways

  • == on primitives compares values. On references it asks "same object?".
  • equals asks "same value?"; the default version from Object is identity.
  • String overrides equals, so text is compared with equals, never with ==.
  • String literals are shared, so == can seem to work and then fail on text built at run time.
  • Write "quit".equals(command) to be safe against null.

Next lesson

The next lesson, "The equals/hashCode contract", shows how to override equals correctly for your own value classes, and why hashCode must always change with it.

Self-check

  1. What does == ask about two objects?
  2. Why does new String("a") == "a" give false?
  3. Which form is null-safe: x.equals("a") or "a".equals(x)?
  4. What does the default equals do?
  5. Why can == on strings pass tests and still be wrong?
Show answers
  1. Whether both references point to the very same object.
  2. new String creates a new object, different from the shared literal.
  3. "a".equals(x).
  4. It behaves like == (identity).
  5. Literals are shared, so tests with literals match; run-time strings are separate objects.

References

  • Oracle, Java Language Specification SE 21, chapter 3: Lexical Structure. Section 3.10.5 says string literals and constant string expressions with the same content refer to the same interned String object, while strings built at run time are new objects.
  • Oracle, Java SE 21 API: java.lang.Object. It states that the default equals is the identity relation.
  • Oracle, Java SE 21 API: java.util.Objects. It describes Objects.equals as safe when either argument is null.
  • Oracle, Java SE 21 API: java.lang.String. It says String.equals is true only for a String with the same sequence of characters, and false for null.
  • That == on references compares the references was not verified on the JLS 15.21.3 page itself (the page text was too large to read through the fetch tool); it follows from the examples above, which were compiled and run on JDK 21.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.