Java String Comparison
Compare Java strings correctly with equals, equalsIgnoreCase, compareTo, and understand why == compares references.
Comparing two strings looks like the most innocent operation in the language, but it's where new Java programmers stub their toe most often. The reason is that == is the obvious-looking syntax for "are these the same?" — and for strings, it answers a question you almost never want answered. This chapter walks through the comparison API in the order you should reach for it, and ends with the rules for ordering — case-folding, locale, numeric strings, and the trap of relying on the JVM's default sort.
Why == is wrong for strings
== compares references for objects. It asks "are these two variables pointing at literally the same object on the heap?". For strings that share identity through the string pool, == happens to return true. For anything else — strings built at runtime, strings parsed from input, strings returned from new String(...) — it returns false even when the contents are identical.
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = "hel" + new String("lo");
a == b; // true — both pooled literals
a == c; // false — c is a fresh object
a == d; // false — d is built at runtime
a.equals(c); // true — contents match
a.equals(d); // true — contents matchThe rule is short and absolute: for string equality, use equals. Always. No matter where the strings came from. == "happens to work" in early tests with literals, then silently goes wrong the first time a string travels through I/O.
equals and equalsIgnoreCase
equals returns true iff the two strings have the same characters in the same order:
"hello".equals("hello"); // true
"hello".equals("Hello"); // false — case-sensitive
"hello".equals(null); // false — never throwsequalsIgnoreCase does a character-by-character comparison after Unicode case-folding:
"hello".equalsIgnoreCase("HELLO"); // true
"hello".equalsIgnoreCase("Hello"); // true
"straße".equalsIgnoreCase("STRASSE"); // false — ß and SS differ in lengthThe German ß example is a useful warning: case-folding in Unicode isn't a bijection (ß lowercases to itself, but it also matches ss in some contracts). If user input might contain non-ASCII text and you need case-insensitive matching, prefer normalising both sides with String#toLowerCase(Locale) before comparing, or use java.text.Collator for locale-aware matching.
null-safe comparison
equals on a null receiver throws NullPointerException. So this is a bug:
if (input.equals("admin")) { ... } // NPE when input is nullThree idiomatic fixes:
"admin".equals(input) // literal on the left — handles null safely
Objects.equals(input, "admin") // null-safe symmetric comparison (java.util.Objects)
Objects.requireNonNull(input).equals("admin") // fail-fast if null is a bug"literal".equals(variable) — sometimes called Yoda comparison — is the most common in established codebases. Objects.equals is slightly nicer when neither side is statically known.
compareTo and compareToIgnoreCase
When you want order (sort, range check, binary search), compareTo returns an int:
< 0if the receiver sorts before the argument0if they are equal> 0if the receiver sorts after the argument
"apple".compareTo("banana"); // negative
"banana".compareTo("apple"); // positive
"apple".compareTo("apple"); // 0The comparison is by Unicode code unit (UTF-16), one character at a time, with shorter strings sorting before longer ones when one is a prefix of the other. That gives you a defined total order — but not the order a human would call "alphabetical":
"Z".compareTo("a"); // negative — 'Z' is 0x5A, 'a' is 0x61
"apple".compareTo("Banana"); // positive — uppercase letters come first in ASCIIFor human-facing sort, two real answers:
compareToIgnoreCasefor the cheap, ASCII-leaning fix that handles English well.java.text.Collatorfor properly locale-aware sort that handles accents, Germanß, the right place forñin Spanish, and the convention that some scripts put numerals after letters.
Collator c = Collator.getInstance(Locale.FRENCH);
List<String> names = new ArrayList<>(List.of("éclair", "Étoile", "anvil"));
names.sort(c); // ["anvil", "éclair", "Étoile"] — proper French sortIf a user is going to see the list, use Collator. If it's a sort key in an internal index, compareTo is faster and stable.
contentEquals and CharSequence
String#contentEquals compares against any CharSequence — String, StringBuilder, StringBuffer, or a CharBuffer. It's the right method when you want to know if a builder's current contents equal a known string without an intermediate toString():
StringBuilder sb = new StringBuilder("hello");
"hello".contentEquals(sb); // true — no toString allocationequals won't help here, because String#equals returns false for any non-String argument by contract.
equals ignores the pool entirely
The pool is an internal storage optimisation; equals doesn't consult it. Two strings with the same characters compare equal whether or not they live at the same address:
String pooled = "x";
String fresh = new String("x");
pooled == fresh; // false — different objects
pooled.equals(fresh); // true — same contents
fresh.hashCode() == pooled.hashCode(); // true — equal strings always hash the sameThat last line is the reason String is safe to use as a HashMap key: equal strings always have equal hash codes, and the cache makes lookups cheap.
A worked example
The program walks through the comparison decisions you'll make in real code: pick the right method for equality, handle null, sort with the right comparator. The output makes the differences between compareTo and a Collator visible side-by-side.
The three sorts make the differences visible. compareTo gives [Banana, anvil, apple, Étoile, éclair]: the uppercase Banana jumps to the front because B (0x42) sorts before any lowercase letter, and Étoile lands near the end because É (0xC9) is above z. CASE_INSENSITIVE_ORDER produces [anvil, apple, Banana, éclair, Étoile], folding case so the list reads alphabetically. On this particular list the French Collator happens to produce the same order — but it is the only one whose result is correct by construction: it folds case and treats accents as a secondary distinction, so it stays right on inputs where the code-point trick breaks (for example, sorting côté relative to cote, or placing ñ correctly in Spanish).
What's next
Splitting a string into pieces is the natural next topic. Java has two tools for it, and one of them is older than the language design wants you to remember. Continue to Java StringTokenizer.