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
nullinstead of throwingNullPointerException. - 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,StringBuilderandStringBufferall implement it. - Hash code
- A number computed from an object's contents, used by
HashMapandHashSetto find the right bucket quickly. - Collator
- A class in
java.textthat 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.
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.
String fromRequest = "superadmin".substring(5); // "admin", but a new object
System.out.println(fromRequest == "admin"); // false
System.out.println(fromRequest.equals("admin")); // true03What 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.
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).
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.
Try it yourself
- 1
Make == lie
In the first example, change
builtto the literal"admin". Predictliteral == built, run, and explain why the result changed even though nothing about the text did. - 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
Sort like a person
Add
"apple pie"and"Apple"tonamesand print both orders again. Predict where"Apple"lands in each before running.
Code & diagrams
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): trueExpected 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 NullPointerExceptionOnly the sign of compareTo is a contract; the exact numbers shown come from the current String implementation.
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 pauseimport 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")).
Break #2
Compare a String with a char
Write if (answer == 'y') where answer is a String.
Break #3
Compare a String with an Integer
Write System.out.println(s == count); where s is "5" and count is an Integer.
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]usingintarithmetic. Because the formula is public and stable, attackers can craft many colliding keys; that's one reason Java 8'sHashMapturns long bucket chains into balanced trees (Topic 9.4). - ▸
javaccompiles a Stringswitchinto alookupswitchonhashCode()followed byequalschecks that set a temporary index, then a secondtableswitchon that index. You can see both withjavap -c. - ▸
String.equalsis a JIT intrinsic on HotSpot: the byte-array comparison runs with vector instructions.equalsis still O(n), so very long keys that share long prefixes (like URLs) are slower map keys than short ones. - ▸
Objects.equals,Objects.hashandObjects.requireNonNullarrived in Java 7'sjava.util.Objects;Objects.requireNonNullElsecame in Java 9. They are the idiomatic null-handling helpers before reaching forOptional(Topic 10.7).
Remember this
- 1
For objects,
==compares references: it istrueonly if both sides point to the same object on the heap.a.equals(b)is a method call that compares contents:String.equalsreturnstruewhenbis also aStringwith the same length and the samecharvalues in the same order. Case matters:"Pune".equals("pune")isfalse. - 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==returnsfalseeven though the text matches. That's how a login check passes every unit test and fails in production. - 3
a.equals(b)throwsNullPointerExceptionifaisnull. Two null-safe styles: put the known literal first ("admin".equals(input)), or usejava.util.Objects.equals(a, b)(Java 7), which returnstruewhen both arenull,falsewhen only one is, and otherwise callsa.equals(b). - 4
Other comparisons have their own methods.
equalsIgnoreCaseignores upper/lower case character by character.contentEquals(CharSequence)compares with aStringBuilderor any otherCharSequence, because"abc".equals(sb)is alwaysfalse(aStringBuilderisn't aString).compareTogives the order: negative, zero or positive, by comparingcharcodes, which is what sorting uses. - 5
compareTois lexicographic by UTF-16 code: it walks both Strings until the first different character and returns the difference of the twocharvalues; 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 useString.CASE_INSENSITIVE_ORDERor a locale-awarejava.text.Collator. - 6
equalsandhashCodego together (Topic 4.8): equal Strings always have equal hash codes, which is whyHashMap<String, ...>andswitchon 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 withequals.
Explain it without notes
What is the difference between == and equals for Strings? Why does == sometimes give the right answer?
How do you compare Strings safely when either might be null?
What does compareTo return, and why does "Zebra" sort before "apple"?
Why is "abc".equals(new StringBuilder("abc")) false, and what should you use instead?
How does switch on a String work under the hood, and what happens with null?
Practice
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.
Sort the array {"delta", "Alpha", "charlie", "Bravo"} first in natural order and then ignoring case, and print both.
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 andequalsis O(n), but correctness wins: useequals. The only valid==uses on Strings are deliberate identity checks after interning, or the fast-path insideequalsitself. - ↔
Yoda style (
"x".equals(s)) avoids exceptions but also hides bugs wherenullshould never appear. Whennullis a programming error, fail fast withObjects.requireNonNull. - ↔
compareTois fast and consistent withequals, but it sorts by code unit, not by language rules. ACollatorgives 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
equalsorObjects.equals.Done when you know when to use
equalsIgnoreCase,contentEqualsandcompareTo.Done when you can explain how a String
switchuseshashCodeandequals.