Command Palette

Search for a command to run...

PHASE 6Intermediate ~29 min· topic 2 of 8

Topic 6.2

== vs equals for Strings

In one line

== asks whether two references point to the very same String object; equals asks whether two Strings hold the same characters. Always compare text with equals (or equalsIgnoreCase, compareTo, Objects.equals), because == only looks right when the pool happens to share an object.

Think of it like this

Two copies of the same storybook. If you ask "is this the same book?", you mean the very same physical copy, the one with your name inside the cover. If you ask "is this the same story?", two copies from the shop both count. Java's == on objects asks the first question (same copy?), and equals asks the second (same story?). For text you almost always mean the second.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Reference equality
Two variables point to the very same object. Tested with ==.
Content equality
Two objects hold the same data, even if they are separate objects. Tested with equals.
Null-safe
Works correctly when a value is null instead of throwing NullPointerException.
Lexicographic order
Dictionary-style order: compare first characters, and only if they match, move on to the next ones.
CharSequence
An interface for any readable sequence of characters. String, StringBuilder and StringBuffer all implement it.
Hash code
A number computed from an object's contents, used by HashMap and HashSet to find the right bucket quickly.
Collator
A class in java.text that compares text the way people in a given language expect, for example treating accents sensibly.

Step by step

01Two questions, two operators

Picture two String objects on the heap, both holding admin. Variable a points to one, b to the other. a == b compares the two arrows: different objects, so false. a.equals(b) walks the characters: same text, so true.

For primitives (int, char, double) there's no object, so == compares values directly. For every object type, including String, Integer and your own classes, == compares references.

Two questions, two operatorsdiagram
Rendering diagram…

02Why == passes your test and fails in production

if (role == "admin") works when role was assigned the literal "admin", because both are the pooled object. When role comes from a request, a database or split, it's a fresh String, and the check silently fails. There is no exception and no warning, only wrong behaviour.

javac won't stop you, because comparing two references is legal. Linters (IntelliJ's "String comparison using ==", Error Prone's ReferenceEquality, SonarQube rule S4973) catch it; leave those checks on.

Main.javawhole filejava
String fromRequest = "superadmin".substring(5);  // "admin", but a new object
System.out.println(fromRequest == "admin");       // false
System.out.println(fromRequest.equals("admin"));  // true

03What String.equals actually does

The JDK implementation is short: if this == anObject, return true straight away (same object, so same contents). Otherwise, if the argument is a String with the same coder (Latin-1 or UTF-16, Topic 6.1) and the same length, compare the byte arrays; any difference returns false.

So equals costs O(n) in the worst case (equal Strings or a late difference), but quickly returns false for different lengths or a wrong type. The JIT replaces the array comparison with a fast vectorised intrinsic.

Because the parameter type is Object, "5".equals(5) compiles and returns false: an Integer is never a String.

04Null-safe comparisons

input.equals("admin") throws when input is null. Flip it: "admin".equals(input) returns false for null, because a literal is never null. People call this "Yoda style".

When neither side is a literal, use Objects.equals(a, b). It's (a == b) || (a != null && a.equals(b)), so two nulls count as equal.

terminal
$ javac Main.java && java Main
── expected output ──
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>" is null
at Main.main(Main.java:4)

05Ignoring case, and comparing with a StringBuilder

equalsIgnoreCase compares character by character, treating A and a as equal (it checks both upper- and lower-case forms of each char). It doesn't use a locale, so it's safe for codes and commands.

Don't write a.toLowerCase().equals(b.toLowerCase()): it creates two new Strings, and toLowerCase() without an argument uses the machine's default locale. In Turkish, "TITLE".toLowerCase() turns I into a dotless ı, so the comparison fails on a Turkish server. If you must lower-case, use toLowerCase(Locale.ROOT).

equals is false for any non-String argument, including a StringBuilder with the same text. contentEquals accepts any CharSequence and compares characters.

06Ordering with compareTo

a.compareTo(b) returns a negative number if a sorts first, 0 if equal, positive if after. Only the sign is guaranteed to mean anything, but the actual value is the difference at the first mismatch: "apple".compareTo("banana") is 'a' - 'b', which is -1. If one is a prefix of the other, it's the length difference: "app".compareTo("apple") is -2.

Because it compares raw char codes, "Zebra" sorts before "apple". compareToIgnoreCase and String.CASE_INSENSITIVE_ORDER fix case; Collator.getInstance(Locale) handles accents and language rules (Phase 9 covers Comparator fully in Topic 9.10).

Main.javawhole filejava
String[] names = {"banana", "apple", "Cherry"};
Arrays.sort(names);                               // [Cherry, apple, banana]
Arrays.sort(names, String.CASE_INSENSITIVE_ORDER); // [apple, banana, Cherry]

07switch on a String uses hashCode and equals

Since Java 7 you can switch on a String. The compiler turns it into a switch on s.hashCode() followed by equals checks inside each bucket, so it's both fast and correct even when two labels share a hash code (like "Aa" and "BB").

A null String in a classic switch throws NullPointerException, because the generated code calls hashCode() on it. Pattern-matching switch (Java 21, Topic 11.2) adds an explicit case null.

switch on a String uses hashCode and equalsdiagram
Rendering diagram…

Try it yourself

  1. 1

    Make == lie

    In the first example, change built to the literal "admin". Predict literal == built, run, and explain why the result changed even though nothing about the text did.

  2. 2

    Find your own hash collisions

    In the third example, print the hash codes of "AaAa", "AaBB", "BBAa" and "BBBB". Predict first: since "Aa" and "BB" collide, what do you expect for these four? Then explain why equal hash codes never prove two Strings are equal.

  3. 3

    Sort like a person

    Add "apple pie" and "Apple" to names and print both orders again. Predict where "Apple" lands in each before running.

Code & diagrams

== looks at objects, equals looks at text New tab
Sign in to run this example in your browser.

Expected output

literal == built: false
literal.equals(built): true
literal == cut: false
literal.equals(cut): true
equals(ADMIN): false
equalsIgnoreCase(ADMIN): true
equals(StringBuilder): false
contentEquals(StringBuilder): true
Null-safe comparisons New tab
Sign in to run this example in your browser.

Expected output

admin -> full access
null  -> read only
Objects.equals(null, null): true
Objects.equals(null, x): false
Objects.equals(x, x): true
a.equals(c) threw NullPointerException
Ordering, hash codes and switch New tab

Only the sign of compareTo is a contract; the exact numbers shown come from the current String implementation.

Sign in to run this example in your browser.

Expected output

apple vs banana: -1
Zebra vs apple: -7
app vs apple: -2
ignoring case, Zebra vs apple: 25
natural order: [Cherry, apple, banana]
case-insensitive: [apple, banana, Cherry]
Aa hash: 2112, BB hash: 2112
Aa equals BB: false
stopping
unknown command pause
Locale-safe case handling (fragment)java
import java.util.Locale;

// Risky: uses the server's default locale (Turkish turns I into a dotless i)
boolean bad = header.toLowerCase().equals("content-type");

// Good: locale-independent comparison
boolean good = header.equalsIgnoreCase("content-type");

// If you need a lower-case key for a map, pin the locale
String key = header.toLowerCase(Locale.ROOT);

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call equals on a null String

Set String input = null; and write if (input.equals("admin")).

terminal
$ javac Main.java && java Main
── what you'll see ──
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>" is null
at Main.main(Main.java:4)

Break #2

Compare a String with a char

Write if (answer == 'y') where answer is a String.

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: bad operand types for binary operator '=='
if (answer == 'y') {
^
first type: String
second type: char
1 error

Break #3

Compare a String with an Integer

Write System.out.println(s == count); where s is "5" and count is an Integer.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: incomparable types: String and Integer
System.out.println(s == count);
^
1 error

Myth vs fact

Myth

== works for Strings because Java pools them.

Fact

Only literals and constant expressions are pooled automatically. Strings from input, files, substring, builders or run-time concatenation are new objects, so == returns false for equal text.

Myth

equalsIgnoreCase is the same as lower-casing both sides.

Fact

toLowerCase() uses the default locale and allocates new Strings; equalsIgnoreCase is locale-independent and allocation-free. They can disagree (for example on a Turkish system).

Myth

Equal hash codes mean equal Strings.

Fact

Different Strings can share a hash code ("Aa" and "BB" are both 2112). Equal Strings must have equal hash codes, not the other way round.

Myth

compareTo returns -1, 0 or 1.

Fact

It returns any negative, zero or positive int. Code that checks == -1 is a bug; check < 0, == 0 or > 0.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    String.hashCode() is specified in the Javadoc: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] using int arithmetic. Because the formula is public and stable, attackers can craft many colliding keys; that's one reason Java 8's HashMap turns long bucket chains into balanced trees (Topic 9.4).

  • ▸

    javac compiles a String switch into a lookupswitch on hashCode() followed by equals checks that set a temporary index, then a second tableswitch on that index. You can see both with javap -c.

  • ▸

    String.equals is a JIT intrinsic on HotSpot: the byte-array comparison runs with vector instructions. equals is still O(n), so very long keys that share long prefixes (like URLs) are slower map keys than short ones.

  • ▸

    Objects.equals, Objects.hash and Objects.requireNonNull arrived in Java 7's java.util.Objects; Objects.requireNonNullElse came in Java 9. They are the idiomatic null-handling helpers before reaching for Optional (Topic 10.7).

Remember this

  1. 1

    For objects, == compares references: it is true only if both sides point to the same object on the heap. a.equals(b) is a method call that compares contents: String.equals returns true when b is also a String with the same length and the same char values in the same order. Case matters: "Pune".equals("pune") is false.

  2. 2

    == on Strings seems to work in small tests because identical literals share one pooled object (Topic 6.1). The moment a String comes from somewhere else (user input, a file, substring, StringBuilder.toString(), concatenation with a variable, JSON parsing), it's a new object, and == returns false even though the text matches. That's how a login check passes every unit test and fails in production.

  3. 3

    a.equals(b) throws NullPointerException if a is null. Two null-safe styles: put the known literal first ("admin".equals(input)), or use java.util.Objects.equals(a, b) (Java 7), which returns true when both are null, false when only one is, and otherwise calls a.equals(b).

  4. 4

    Other comparisons have their own methods. equalsIgnoreCase ignores upper/lower case character by character. contentEquals(CharSequence) compares with a StringBuilder or any other CharSequence, because "abc".equals(sb) is always false (a StringBuilder isn't a String). compareTo gives the order: negative, zero or positive, by comparing char codes, which is what sorting uses.

  5. 5

    compareTo is lexicographic by UTF-16 code: it walks both Strings until the first different character and returns the difference of the two char values; if one String is a prefix of the other, it returns the difference in lengths. So every upper-case letter sorts before every lower-case letter ('Z' is 90, 'a' is 97). For human-friendly order use String.CASE_INSENSITIVE_ORDER or a locale-aware java.text.Collator.

  6. 6

    equals and hashCode go together (Topic 4.8): equal Strings always have equal hash codes, which is why HashMap<String, ...> and switch on a String (Java 7) work. The reverse isn't true: "Aa" and "BB" have the same hash code (2112) but aren't equal, so hash-based code always confirms a match with equals.

Explain it without notes

01

What is the difference between == and equals for Strings? Why does == sometimes give the right answer?

02

How do you compare Strings safely when either might be null?

03

What does compareTo return, and why does "Zebra" sort before "apple"?

04

Why is "abc".equals(new StringBuilder("abc")) false, and what should you use instead?

05

How does switch on a String work under the hood, and what happens with null?

Practice

01

Write static boolean isYes(String answer) that returns true for "yes", "YES" or "Yes" (with surrounding spaces allowed) and false for null. Test it with four inputs.

02

Sort the array {"delta", "Alpha", "charlie", "Bravo"} first in natural order and then ignoring case, and print both.

03

Write a program that shows == failing for a String read from "name=Ravi".split("=")[1] while equals succeeds.

Trade-offs

  • ↔

    == is a single pointer comparison and equals is O(n), but correctness wins: use equals. The only valid == uses on Strings are deliberate identity checks after interning, or the fast-path inside equals itself.

  • ↔

    Yoda style ("x".equals(s)) avoids exceptions but also hides bugs where null should never appear. When null is a programming error, fail fast with Objects.requireNonNull.

  • ↔

    compareTo is fast and consistent with equals, but it sorts by code unit, not by language rules. A Collator gives human order but is slower and its result depends on the locale.

Done when you can

  • Done when you can explain reference equality vs content equality with a diagram.

  • Done when you can say exactly when == on Strings appears to work and why it's still wrong.

  • Done when you compare nullable Strings with a literal-first equals or Objects.equals.

  • Done when you know when to use equalsIgnoreCase, contentEquals and compareTo.

  • Done when you can explain how a String switch uses hashCode and equals.