String compareTo is one of those Java methods that looks deceptively simple — it compares two strings and returns a number — but produces surprising results when you hit case differences, Unicode characters, invisible whitespace, or try to use it for sorting. This guide covers java string compareTo exhaustively: return values, method signatures, the difference between compareTo and equals, case sensitivity pitfalls, sorting with the Comparable interface, debugging when comparisons mysteriously fail, and how the same concept maps to Kotlin, C#, and Python. Every section includes copy-paste code examples.
What Is compareTo() and What Does It Return?
String.compareTo() is a method defined in the java.lang.String
class that performs a lexicographic comparison of two strings. Lexicographic
order is the order you would find in a dictionary: character by character, from left to
right, using the Unicode code point of each character.
The return value is an int with three meaningful ranges:
| Return value | Meaning | Example |
|---|---|---|
0 | The strings are equal | "apple".compareTo("apple") → 0 |
| Negative integer | Calling string comes before the argument alphabetically | "apple".compareTo("banana") → -1 |
| Positive integer | Calling string comes after the argument alphabetically | "banana".compareTo("apple") → 1 |
The exact non-zero value is the difference of the Unicode code point values at the first position where the strings differ, or the difference in string lengths if one string is a prefix of the other. For example:
// 'b' (98) - 'a' (97) = 1
"banana".compareTo("apple"); // positive — could be 1
// "app" is a prefix of "apple"; length diff = 3 - 5 = -2
"app".compareTo("apple"); // -2 (length of "app" minus length of "apple")
// equal
"hello".compareTo("hello"); // 0 Key rule: Only rely on the sign of the return value, not its
exact magnitude. The JDK specification only guarantees negative/zero/positive. Code like
if (a.compareTo(b) == -1) is fragile and breaks when the strings differ at
a position other than the first character.
compareTo() Syntax and Method Signatures
String provides two variants. Both are instance methods — you call them
on a String object and pass the comparison target as an argument.
// Case-sensitive lexicographic comparison
public int compareTo(String anotherString)
// Case-insensitive lexicographic comparison
public int compareToIgnoreCase(String str) String also implements java.lang.Comparable<String>,
so compareTo satisfies the contract for sorted collections, binary search,
and any API that accepts a Comparable.
Basic usage examples
String a = "mango";
String b = "orange";
String c = "mango";
System.out.println(a.compareTo(b)); // negative (m < o)
System.out.println(b.compareTo(a)); // positive (o > m)
System.out.println(a.compareTo(c)); // 0 (equal)
System.out.println(a.compareToIgnoreCase("MANGO")); // 0 The method inside java.lang.String
Under the hood, the OpenJDK implementation of compareTo iterates over
characters with a tight loop and exits early on the first mismatch — O(n) worst-case,
O(1) best-case when the first characters differ. The implementation looks roughly like
this (simplified):
public int compareTo(String anotherString) {
int len1 = this.length();
int len2 = anotherString.length();
int minLen = Math.min(len1, len2);
for (int i = 0; i < minLen; i++) {
char c1 = this.charAt(i);
char c2 = anotherString.charAt(i);
if (c1 != c2) return c1 - c2;
}
return len1 - len2;
} The actual OpenJDK source uses intrinsics and SIMD optimizations on modern JVMs, so string comparison is much faster than a character-by-character loop would suggest.
compareTo() vs equals() vs == — The Full Picture
This is the question every Java developer asks. The three mechanisms test different things and are not interchangeable. For a deeper look at the full Java string comparison API, see our guide on Java String Compare: equals(), compareTo() & More.
| Mechanism | Tests | Return type | Null safe? | Use when |
|---|---|---|---|---|
== | Reference identity | boolean | Yes | Never for string content |
.equals() | Content equality | boolean | No (NPE on null receiver) | Simple equality check |
.compareTo() | Lexicographic order | int | No (NPE on null) | Sorting, ordering, Comparable |
The == trap
String x = new String("hello");
String y = new String("hello");
System.out.println(x == y); // false — different objects
System.out.println(x.equals(y)); // true — same content
System.out.println(x.compareTo(y)); // 0 — same content, same order == compares memory addresses. Two String objects constructed
with new String() are always distinct objects even if they contain identical
characters. String literals benefit from the JVM's string pool, so
"hello" == "hello" often returns true — but that is an
implementation detail, never a guarantee. Always use equals() or
compareTo() for content comparisons.
When compareTo() == 0 is not the same as equals()
For java.lang.String, compareTo() == 0 and
equals() always agree. However, this is not true for all classes. The
Java documentation warns that it is "strongly recommended" — but not required
— that a class's natural ordering (defined by compareTo) be consistent with
equals. The BigDecimal class is a famous counterexample:
new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) returns 0, but
equals() returns false because the scales differ.
For strings specifically, prefer equals() when all you need is a
true/false answer — it communicates intent clearly and is marginally faster because
it can short-circuit on object identity (this == anotherString) before
doing any character work.
Case Sensitivity and Unicode Edge Cases
compareTo() is case-sensitive by default because uppercase ASCII letters
('A'–'Z', code points 65–90) have lower Unicode values than their lowercase equivalents
('a'–'z', code points 97–122). This means uppercase strings sort before
lowercase strings in natural order:
System.out.println("Apple".compareTo("apple")); // negative (-32)
System.out.println("Zebra".compareTo("apple")); // negative — 'Z' (90) < 'a' (97)
System.out.println("apple".compareTo("Zebra")); // positive
That last line surprises most developers: "apple" sorts after "Zebra" in natural order because lowercase 'a' has a higher code point
than uppercase 'Z'. This is rarely the ordering users expect in a UI.
compareToIgnoreCase()
System.out.println("Apple".compareToIgnoreCase("apple")); // 0
System.out.println("Zebra".compareToIgnoreCase("apple")); // positive (Z > a, ignoring case) compareToIgnoreCase() folds each character to its uppercase form before
comparing, per Character.toUpperCase(). This works correctly for ASCII
and most Western European characters. It does not handle all locale-specific
rules:
- Turkish I problem: In Turkish locale, the uppercase of 'i' is 'İ'
(with a dot), and the lowercase of 'I' is 'ı' (without a dot). Using
compareToIgnoreCasewith Turkish strings can produce wrong results. Use a locale-awareCollatorinstead. - German sharp-S (ß): 'ß' uppercases to "SS" in some contexts. A character-by-character case fold can miss this.
- Unicode normalization: The character 'é' can be represented as a
single code point (U+00E9, precomposed) or as 'e' followed by a combining accent
(U+0065 + U+0301, decomposed). These look identical but
compareTo()treats them as different strings per the Unicode Normalization Forms specification. Normalize withjava.text.Normalizer.normalize(s, Form.NFC)before comparing.
import java.text.Normalizer;
String precomposed = "\u00e9"; // é as one code point
String decomposed = "e\u0301"; // e + combining accent
System.out.println(precomposed.compareTo(decomposed)); // non-zero — different!
// Normalize first
String n1 = Normalizer.normalize(precomposed, Normalizer.Form.NFC);
String n2 = Normalizer.normalize(decomposed, Normalizer.Form.NFC);
System.out.println(n1.compareTo(n2)); // 0 When dealing with user-facing text from web forms or databases, always normalize to NFC before any string comparison. For general string comparison strategies across languages see our broader guide.
Sorting with compareTo() and the Comparable Interface
The primary use case for compareTo() is sorting. Because
String implements Comparable<String>,
Collections.sort(), Arrays.sort(), TreeSet,
and TreeMap all use compareTo() internally for natural
ordering.
Sorting a list of strings
import java.util.*;
List<String> fruits = new ArrayList<>(Arrays.asList(
"orange", "Apple", "banana", "cherry"
));
// Natural order (case-sensitive, uppercase first)
Collections.sort(fruits);
System.out.println(fruits); // [Apple, banana, cherry, orange]
// Case-insensitive alphabetical order
fruits.sort(String::compareToIgnoreCase);
System.out.println(fruits); // [Apple, banana, cherry, orange] Implementing Comparable in a custom class
public class Product implements Comparable<Product> {
private final String name;
private final double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public int compareTo(Product other) {
// Sort by name alphabetically, then by price ascending
int nameOrder = this.name.compareToIgnoreCase(other.name);
if (nameOrder != 0) return nameOrder;
return Double.compare(this.price, other.price);
}
}
Returning this.name.compareTo(other.name) from your
compareTo implementation is the idiomatic way to delegate string ordering
without reimplementing comparison logic yourself.
Chaining Comparators (Java 8+)
For more complex sort keys, the Comparator API is more readable than
nested compareTo() calls:
List<Product> products = /* ... */;
products.sort(
Comparator.comparing(Product::getName, String::compareToIgnoreCase)
.thenComparingDouble(Product::getPrice)
); TreeMap and TreeSet
// TreeMap uses compareTo() to maintain key order
TreeMap<String, Integer> inventory = new TreeMap<>();
inventory.put("banana", 50);
inventory.put("Apple", 30);
inventory.put("cherry", 10);
// Keys are iterated in natural (compareTo) order: Apple, banana, cherry
inventory.forEach((k, v) -> System.out.println(k + ": " + v));
Pass a custom Comparator to the TreeMap constructor to
override the natural compareTo() ordering — for example,
new TreeMap<>(String.CASE_INSENSITIVE_ORDER).
Debugging compareTo() Failures: Invisible Characters and Visual Diffs
This is the section most tutorials skip. You run compareTo(), get a
non-zero result, and the strings look absolutely identical in your IDE's
console output. Here are the most common causes and how to diagnose each one.
1. Trailing or leading whitespace
The most common culprit. A string returned from a database, API response, or user input often has a trailing space or newline that is invisible in most log outputs.
String fromDb = "status "; // trailing space from VARCHAR column
String literal = "status";
System.out.println(fromDb.compareTo(literal)); // non-zero!
System.out.println(fromDb.trim().compareTo(literal)); // 0
Always call .strip() (Java 11+, Unicode-aware) or .trim()
on values from external sources before comparing.
2. Zero-width characters and non-breaking spaces
Copy-pasted text from web pages, Word documents, or messaging apps frequently contains invisible Unicode characters:
- U+00A0 — Non-breaking space (looks like a space, but
trim()does not remove it) - U+200B — Zero-width space (completely invisible)
- U+FEFF — BOM (Byte Order Mark, often prepended to files)
- U+200C, U+200D — Zero-width non-joiner / joiner
String clean = "hello";
String hidden = "hello\u200B"; // zero-width space at end
System.out.println(clean.compareTo(hidden)); // non-zero
System.out.println(clean.length() + " vs " + hidden.length()); // 5 vs 6
// Detect and strip
String stripped = hidden.replaceAll("[\\p{Cf}]", ""); // remove Unicode format chars
System.out.println(clean.compareTo(stripped)); // 0 3. Unicode normalization mismatches
As covered in the previous section, visually identical characters can have different byte representations. This is especially common with accented characters and emoji with skin tone modifiers.
4. Character encoding issues
If strings originate from byte arrays decoded with different charsets (e.g., one decoded as UTF-8 and another as ISO-8859-1), characters above U+007F will have different code point values even though the bytes "looked" the same on disk.
Using a visual diff tool to expose hidden differences
When System.out.println(a + " | " + b) shows two identical-looking
strings, a visual diff tool is the fastest path to finding the hidden character.
The Diff Checker Chrome extension lets you paste both strings
side-by-side and highlights every character difference — including invisible
whitespace, zero-width characters, and Unicode variants — with color coding and
a plain-English summary. No manual hex dump needed.
This is the same technique covered in our C++ string compare debugging guide — the pattern applies equally in Java: paste both strings into the diff tool, let it highlight the discrepancy, then fix the normalization or trim logic.
5. Diagnostic code snippet
public static void diagnoseCompareTo(String a, String b) {
System.out.println("compareTo result : " + a.compareTo(b));
System.out.println("Length a=" + a.length() + " b=" + b.length());
int minLen = Math.min(a.length(), b.length());
for (int i = 0; i < minLen; i++) {
if (a.charAt(i) != b.charAt(i)) {
System.out.printf(
"First diff at index %d: a=U+%04X b=U+%04X%n",
i, (int) a.charAt(i), (int) b.charAt(i)
);
return;
}
}
if (a.length() != b.length()) {
System.out.println("One string is a prefix of the other (length diff)");
} else {
System.out.println("Strings are equal by compareTo");
}
} This snippet prints the index and Unicode code points of the first differing character, which is usually enough to identify the problem instantly.
compareTo() Across Languages: Java, Kotlin, C#, Python
The concept of lexicographic string comparison exists in every mainstream language, though the method names and return-value conventions vary.
Kotlin
Kotlin strings inherit Java's compareTo() because they compile to
java.lang.String on the JVM. Kotlin also exposes comparison operators
directly on strings:
val a = "apple"
val b = "banana"
println(a.compareTo(b)) // negative
println(a.compareTo(b, ignoreCase = true)) // negative (still a < b)
println(a < b) // true — uses compareTo internally
println(a == b) // false — structural equality in Kotlin
In Kotlin, == is structurally equivalent to Java's equals(),
which is one of the language's most developer-friendly improvements over Java.
C# — String.CompareTo()
string a = "apple";
string b = "banana";
int result = a.CompareTo(b); // negative
int result2 = string.Compare(a, b, StringComparison.OrdinalIgnoreCase); // negative
// C# also supports relational operators on strings
bool less = string.Compare(a, b, StringComparison.Ordinal) < 0; // true
C# has two main comparison APIs: the instance method CompareTo() (which
uses culture-sensitive comparison by default) and the static
string.Compare() (which accepts a StringComparison enum
for explicit control). Always pass a StringComparison value in C# to
avoid unexpected locale-sensitive behavior in production.
Python
Python has no compareTo() method. Strings support relational operators
(<, >, ==) directly, and the built-in
sorted() function uses these operators. For locale-aware comparison,
use the locale module or the third-party PyICU library:
a = "apple"
b = "banana"
print(a < b) # True — lexicographic
print(a == b) # False
# Equivalent to Java's compareTo sign:
result = (a > b) - (a < b) # -1, 0, or 1 JavaScript
JavaScript uses String.prototype.localeCompare() as the closest
equivalent, returning a negative, zero, or positive number. See our
equal sign in JavaScript guide
for the full picture of JS comparison operators, and our
JavaScript string equals
article for how ===, localeCompare, and
Intl.Collator fit together.
const a = "apple";
const b = "banana";
console.log(a.localeCompare(b)); // -1 (or negative in some engines)
console.log(a < b); // true — lexicographic via relational operators Best Practices and Performance Notes
Choosing the right method
| Scenario | Recommended method |
|---|---|
| Test equality (case-sensitive) | a.equals(b) |
| Test equality (case-insensitive) | a.equalsIgnoreCase(b) |
| Null-safe equality | Objects.equals(a, b) |
| Sort / order strings | a.compareTo(b) |
| Sort case-insensitively | a.compareToIgnoreCase(b) |
| Sort with locale rules (accents, language) | Collator.getInstance(locale).compare(a, b) |
| Null-safe ordering | Comparator.nullsFirst(Comparator.naturalOrder()) |
Null safety
compareTo() throws NullPointerException if either string
is null — both null.compareTo(x) and x.compareTo(null)
are invalid. The safest patterns:
// Pattern 1: Comparator with null handling (Java 8+)
Comparator<String> safe = Comparator.nullsFirst(Comparator.naturalOrder());
safe.compare(null, "hello"); // null sorts first — no NPE
// Pattern 2: Explicit guard
int safeCompare(String a, String b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;
return a.compareTo(b);
} Performance considerations
- Early exit on length:
compareTo()does not short-circuit on length mismatch (unlikeequals()). If you need pure equality,equals()is always at least as fast — often faster because it checks length before iterating characters. - String interning: Interned strings allow
==equality checks, which are O(1) reference comparisons. But interning has its own cost (hash-table lookup onintern()calls), so it is only worthwhile for very large pools of repeated strings. - Collator caching:
Collator.getInstance()is relatively expensive. Cache theCollatorinstance rather than constructing it on every comparison call. - Avoid toLowerCase() + equals(): This pattern allocates a new
string on every call. Use
equalsIgnoreCase()orcompareToIgnoreCase()which compare in-place without allocation.
Common anti-patterns
// WRONG: relies on exact return value
if (a.compareTo(b) == -1) { ... } // fragile
// CORRECT: relies on sign only
if (a.compareTo(b) < 0) { ... }
// WRONG: allocates intermediate strings
if (a.toLowerCase().equals(b.toLowerCase())) { ... }
// CORRECT: in-place case-insensitive
if (a.equalsIgnoreCase(b)) { ... }
// WRONG: ignores possible null
if (userInput.compareTo(expected) == 0) { ... }
// CORRECT: null-safe equality for simple checks
if (Objects.equals(userInput, expected)) { ... } Frequently Asked Questions
What does compareTo() return in Java?
String.compareTo() returns an int: 0
if the strings are equal, a negative integer if the calling
string is lexicographically less than the argument (comes before it in
dictionary order), and a positive integer if it is greater
(comes after). The exact non-zero value is the Unicode code point difference
at the first differing position, or the length difference if one string is a
prefix of the other. Rely only on the sign — not the magnitude.
What is the difference between compareTo() and equals() in Java?
equals() returns a boolean — just true or false — and
is the right choice when you only need to know if two strings are identical.
compareTo() returns an int that encodes ordering
information: is string A less than, equal to, or greater than string B? Use
equals() for equality checks and compareTo() for
sorting or implementing Comparable. They agree on equality for
java.lang.String, but compareTo() == 0 is less
readable than equals() for that purpose.
Does compareTo() handle null in Java?
No. Both null.compareTo(s) and s.compareTo(null)
throw NullPointerException. For null-safe ordering use
Comparator.nullsFirst(Comparator.naturalOrder()) or
Comparator.nullsLast(), or guard with an explicit null check
before calling compareTo().
How do I sort a list of strings using compareTo()?
Call Collections.sort(list) or list.sort(null) for
natural (lexicographic) order — both use String.compareTo()
internally. For case-insensitive alphabetical order use
list.sort(String::compareToIgnoreCase). For locale-aware sorting
that correctly handles accented characters use
list.sort(Collator.getInstance(locale)::compare).
What is the difference between compareTo() and compareToIgnoreCase()?
compareTo() is case-sensitive — uppercase letters (code points
65–90) sort before lowercase (97–122), so "Apple" comes before
"banana" in natural order.
compareToIgnoreCase() folds each character to its uppercase form
before comparing, so "Apple".compareToIgnoreCase("apple") returns
0. Neither method is locale-aware; use a Collator for
language-specific case rules (Turkish, German, etc.).
See String Differences Instantly
Stop squinting at console output. Diff Checker highlights every character difference side-by-side — invisible chars, whitespace, Unicode variants — in one click.
Install Free — Chrome Web Store