Java String Comparison: equals, compareTo, and Collator
Comparing strings correctly in Java is surprisingly nuanced. The == operator checks reference equality, not content: a common source of bugs. equals() compares content, compareTo() provides lexicographic ordering, and Collator handles locale-aware sorting. Knowing which to use in which situation prevents subtle bugs and incorrect sort orders.
Contents
equals() vs ==
The ====truenew String(...)==falseequals()equalsIgnoreCase()Collator
String a = "hello";
String b = "hello";
String c = new String("hello"); // explicitly creates a new object
// == compares references (memory addresses)
System.out.println(a == b); // true — string pool: same object
System.out.println(a == c); // false — different object in heap
System.out.println(a == "hello");// true — string pool literal
// equals() compares content
System.out.println(a.equals(b)); // true
System.out.println(a.equals(c)); // true — content is same
System.out.println(a.equals(null)); // false (no NPE)
// equalsIgnoreCase() — case-insensitive content comparison
String s1 = "Java";
String s2 = "JAVA";
System.out.println(s1.equalsIgnoreCase(s2)); // true
// NB: equalsIgnoreCase is NOT locale-aware — use Collator for correct locale behavior
// contentEquals — compare with CharSequence (StringBuilder etc.)
StringBuilder sb = new StringBuilder("hello");
System.out.println(a.equals(sb)); // false — different types
System.out.println(a.contentEquals(sb)); // true — content comparison
// Safe pattern — put the known non-null string on the left to avoid NPE
String input = getUserInput(); // may be null
if ("ACTIVE".equals(input)) { /* ... */ } // safe
// if (input.equals("ACTIVE")) { } // NPE if input is null
// Objects.equals — null-safe, does deep content comparison
System.out.println(Objects.equals(null, null)); // true
System.out.println(Objects.equals("hi", null)); // false
System.out.println(Objects.equals("hi", "hi")); // true
==new String(...)==falsecompareTo() and compareToIgnoreCase()
compareTo()0compareToIgnoreCase()Collator
// compareTo() — lexicographic comparison based on Unicode code points
// Returns: negative if this < other, 0 if equal, positive if this > other
String x = "apple";
String y = "banana";
int result = x.compareTo(y); // negative — 'a' (97) < 'b' (98)
System.out.println(result); // e.g. -1
System.out.println("abc".compareTo("abc")); // 0 — equal
System.out.println("abc".compareTo("abd")); // -1 — 'c' < 'd'
System.out.println("abd".compareTo("abc")); // 1 — 'd' > 'c'
System.out.println("abc".compareTo("abcd")); // -1 — shorter is "less"
// Case sensitivity: uppercase letters come before lowercase in Unicode
System.out.println("Z".compareTo("a")); // negative! 'Z'=90, 'a'=97
// compareToIgnoreCase() — case-insensitive lexicographic order
System.out.println("Z".compareToIgnoreCase("a")); // positive — 'z' > 'a'
System.out.println("Apple".compareToIgnoreCase("apple")); // 0
// Using compareTo() in sorting
List<String> words = new ArrayList<>(List.of("banana", "Apple", "cherry"));
Collections.sort(words); // ["Apple", "banana", "cherry"]
Collections.sort(words, String::compareToIgnoreCase); // ["Apple", "banana", "cherry"]
// For locale-correct sort, use Collator instead (see below)
// Comparator using String comparison
Comparator<String> byLength = Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder());
words.sort(byLength);
contains, startsWith, endsWith, matches
These methods return a boolean and are case-sensitive. contains()startsWith()endsWith()startsWith(prefix, offset)matches()Pattern.matcher(s).find()matches()contains()indexOf()
String s = "The quick brown fox";
// contains — substring check
System.out.println(s.contains("quick")); // true
System.out.println(s.contains("slow")); // false
// contains(CharSequence) — works with String, StringBuilder, etc.
// startsWith / endsWith
System.out.println(s.startsWith("The")); // true
System.out.println(s.startsWith("quick", 4));// true — starting from index 4
System.out.println(s.endsWith("fox")); // true
// indexOf — position of substring (-1 if not found)
System.out.println(s.indexOf("quick")); // 4
System.out.println(s.indexOf("o")); // 11 (first occurrence)
System.out.println(s.lastIndexOf("o")); // 17 (last occurrence)
// matches — check against a regex pattern
System.out.println("hello123".matches("[a-z]+\\d+")); // true
System.out.println("hello".matches("[a-z]+")); // true
// matches() must match the ENTIRE string (anchored implicitly)
System.out.println("hello world".matches("[a-z]+")); // false (space not matched)
// Use contains() or Pattern.matcher().find() for partial matches
// regionMatches — compare subregions of two strings
String s1 = "hello world";
String s2 = "WORLD";
System.out.println(s1.regionMatches(6, "world", 0, 5)); // true (case-sensitive)
System.out.println(s1.regionMatches(true, 6, s2, 0, 5)); // true (ignoreCase=true)
Locale-aware Comparison with Collator
String.compareTo()Collatorjava.textCollator.getInstance(Locale)ComparatorCollationKey
import java.text.Collator;
import java.util.Locale;
// String.compareTo() uses Unicode code points — not suitable for human-readable sorting
// "ä" comes after "z" in Unicode but should sort near "a" in German
List<String> names = new ArrayList<>(List.of("Müller", "Maier", "Ärger", "Bauer"));
// Raw sort — wrong for German names
Collections.sort(names);
System.out.println(names); // [Bauer, Maier, Müller, Ärger] — "Ä" sorts last, wrong
// Collator — locale-aware, correct linguistic sort
Collator collator = Collator.getInstance(Locale.GERMAN);
names.sort(collator);
System.out.println(names); // [Ärger, Bauer, Maier, Müller] — correct
// Collator strength levels
Collator primary = Collator.getInstance(Locale.ENGLISH);
primary.setStrength(Collator.PRIMARY); // "a" == "A" == "ä"
primary.setStrength(Collator.SECONDARY); // "a" == "A", but "a" != "ä"
primary.setStrength(Collator.TERTIARY); // default: "a" != "A", "a" != "ä"
// Collator.compare() — same contract as compareTo()
int cmp = collator.compare("Müller", "Maier"); // negative in German sort
// Pre-compute sort keys for large datasets (expensive comparison is done once)
Collator.CollationKey key1 = collator.getCollationKey("Müller");
Collator.CollationKey key2 = collator.getCollationKey("Maier");
int result = key1.compareTo(key2);
CollatorcompareTo()Null-safe Comparison
Calling equals()compareTo()nullNullPointerException"ACTIVE".equals(input)Objects.equals(a, b)String.valueOf(obj)"null"Comparator.nullsFirst()Comparator.nullsLast()
// Calling methods on a null String throws NullPointerException
String s = null;
// s.equals("hello"); // NPE!
// s.compareTo("hello"); // NPE!
// Safe patterns:
// 1. Put the literal or known-non-null string on the left
"hello".equals(s); // false, no NPE
"hello".equalsIgnoreCase(s); // false, no NPE
// 2. Objects.equals — both arguments may be null
Objects.equals(s, "hello"); // false
Objects.equals(null, null); // true
// 3. String.valueOf — converts null to the string "null"
String safe = String.valueOf(s); // "null"
// Null-safe comparator for sorting (nulls first or last)
List<String> withNulls = new ArrayList<>(Arrays.asList("b", null, "a", null, "c"));
withNulls.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(withNulls); // [null, null, a, b, c]
withNulls.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(withNulls); // [a, b, c, null, null]