String Compare
Comparing strings answers two different questions: are these two strings the same text? (equality) and which one comes first? (ordering). Java provides separate methods for each, with case-sensitive and case-insensitive variants, plus Collator for language-aware ordering. Using the right one, and never using == for content, prevents some of the most common bugs in Java programs.
Equality: equals, Not ==
String s1 = "hello";
String s3 = new String("hello");
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s3)); // true
==compares references: whether both variables point to the same object.equalscompares content: whether both strings contain the same characters in the same order.
Two strings with identical text are often different objects, especially when one was read from a file, typed by a user, received over the network, or built at runtime. == then returns false even though the text matches. Because string literals are pooled, == sometimes appears to work in small tests, which makes the bug easy to miss and hard to find later.
Rule: always use equals (or a variant) to compare string content.
Null-safe equality
Calling equals on a null reference throws NullPointerException. Two safe patterns:
String input = null;
"yes".equals(input); // false: the literal is never null
Objects.equals(input, "yes"); // false: handles null on either side
Objects.equals(null, null); // true
Putting the known non-null value (often a literal or constant) on the left is a common Java idiom.
Ignoring case
"Hello".equalsIgnoreCase("HELLO"); // true
equalsIgnoreCase compares character by character, treating upper- and lowercase forms as equal. It is the right tool for comparing user input against fixed keywords ("yes", "quit"), file extensions, and HTTP header names.
Comparing with other CharSequences
equals returns false for anything that is not a String, even a StringBuilder with the same text. Use contentEquals:
"hello".equals(new StringBuilder("hello")); // false: different types
"hello".contentEquals(new StringBuilder("hello")); // true
Ordering: compareTo
compareTo determines which string comes first, character by character, using each character's UTF-16 code:
int r = a.compareTo(b);
// r < 0 → a comes before b
// r == 0 → same text
// r > 0 → a comes after b
"apple".compareTo("apricot"); // -2: 'p' (112) vs 'r' (114) at index 2
"app".compareTo("apple"); // -2: "app" is a prefix, so shorter comes first (3 - 5)
"B".compareToIgnoreCase("a"); // 1: 'b' after 'a' when case is ignored
The algorithm finds the first position where the strings differ and returns the difference between those two characters. If one string is a prefix of the other, it returns the difference in lengths. Only the sign of the result is meaningful; do not test for specific values like -1.
compareTo returns 0 exactly when equals returns true, so the two are consistent.
Code-point order is not alphabetical order
Because compareTo compares character codes, it puts all uppercase letters (A–Z, 65–90) before all lowercase letters (a–z, 97–122), and places accented letters (such as É, 201) after both:
List<String> names = new ArrayList<>(List.of("émile", "Zara", "adam", "Émile", "zoe", "Ada", "ada"));
Collections.sort(names);
// [Ada, Zara, ada, adam, zoe, Émile, émile]
"Zara" before "ada" is correct by code point, but no human would sort a contact list that way.
Case-Insensitive Ordering
String.CASE_INSENSITIVE_ORDER is a ready-made comparator, and compareToIgnoreCase is the method equivalent:
names.sort(String.CASE_INSENSITIVE_ORDER);
// [Ada, ada, adam, Zara, zoe, émile, Émile]
Case no longer splits the list, but accented letters still sort after z.
Language-Aware Ordering: Collator
java.text.Collator compares strings according to the rules of a natural language, the way dictionaries and phone books do:
Collator collator = Collator.getInstance(Locale.ENGLISH);
names.sort(collator);
// [ada, Ada, adam, émile, Émile, Zara, zoe]
Now émile sorts with the es, and case is treated as a minor difference rather than a major one.
A collator's strength controls which differences matter:
| Strength | Distinguishes | "a" vs "A" | "e" vs "é" |
|---|---|---|---|
PRIMARY | Base letters only | equal | equal |
SECONDARY | Base letters and accents | equal | different |
TERTIARY (default) | Base letters, accents, and case | different | different |
PRIMARY strength is useful for "these are effectively the same name" checks, such as finding duplicates that differ only in case or accents.
Use Collator for anything shown to users in sorted order: names, cities, product titles. Use plain compareTo for machine-oriented ordering (IDs, codes, keys), where consistency and speed matter more than linguistic conventions.
Comparing Parts of Strings
| Method | Checks |
|---|---|
startsWith(prefix) / endsWith(suffix) | Beginning or end |
contains(s) | Anywhere |
regionMatches(ignoreCase, offset, other, otherOffset, len) | A region of one string against a region of another |
matches(regex) | Whole string against a pattern |
"Java Tutorial".regionMatches(true, 5, "TUTOR", 0, 5); // true: case-insensitive region match
regionMatches compares part of a string without creating a substring.
Normalizing Before Comparing
User-entered text often differs in ways that should not matter. Normalize both sides first:
static boolean sameEmail(String a, String b) {
return a.strip().equalsIgnoreCase(b.strip());
}
For text with accents, Unicode normalization (java.text.Normalizer) makes visually identical strings compare equal even if they were typed with different combinations of characters, such as a precomposed é versus e followed by a combining accent.
Summary of Methods
| Goal | Method |
|---|---|
| Same text, exact | a.equals(b) |
| Same text, null-safe | Objects.equals(a, b) or "literal".equals(x) |
| Same text, ignoring case | a.equalsIgnoreCase(b) |
Same text as a StringBuilder | a.contentEquals(sb) |
| Order, by code point | a.compareTo(b) |
| Order, ignoring case | a.compareToIgnoreCase(b) / String.CASE_INSENSITIVE_ORDER |
| Order, as a human would expect | Collator.getInstance(locale) |
| Same object (rarely needed) | a == b |
Practical Example: Sorting a Contact List
import java.text.Collator;
import java.util.*;
public class Contacts {
public static void main(String[] args) {
List<String> names = List.of("émile", "Zara", "adam", "Émile", "zoe", "Ada", "ada");
List<String> natural = new ArrayList<>(names);
Collections.sort(natural);
System.out.println("compareTo: " + natural);
List<String> ignoreCase = new ArrayList<>(names);
ignoreCase.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println("ignore case: " + ignoreCase);
Collator collator = Collator.getInstance(Locale.ENGLISH);
List<String> human = new ArrayList<>(names);
human.sort(collator);
System.out.println("Collator: " + human);
collator.setStrength(Collator.PRIMARY);
Set<String> unique = new TreeSet<>(collator);
unique.addAll(names);
System.out.println("unique (primary): " + unique);
}
}
Output:
compareTo: [Ada, Zara, ada, adam, zoe, Émile, émile]
ignore case: [Ada, ada, adam, Zara, zoe, émile, Émile]
Collator: [ada, Ada, adam, émile, Émile, Zara, zoe]
unique (primary): [Ada, adam, émile, Zara, zoe]
The same seven names, sorted three ways, show why the choice of comparison matters:
compareTofollows character codes. Uppercase names come first, then lowercase, and accented names end up last.- Case-insensitive ordering fixes the uppercase/lowercase split but still puts
émileafterzoe. - The
Collatorproduces the order a person would expect in an address book.
The last line uses a PRIMARY-strength collator as the TreeSet's comparator. At that strength, Ada/ada and émile/Émile compare as equal, so the set keeps only the first of each and acts as a duplicate detector that ignores both case and accents.