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?

0 EQUAL Negative integer "apple".compareTo("banana") → −1 String A < String B Positive integer "banana".compareTo("apple") → +1 String A > String B −N +N Only the sign matters — do not rely on the exact magnitude

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

compareTo(String) public int compareTo( String other) Case-sensitive Returns: int compareToIgnoreCase(String) public int compareToIgnore Case(String str) Case-insensitive Returns: int vs

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

Need ordering? (sort / rank) Yes compareTo() returns int No Need equality? (same content?) Yes equals() returns boolean No Never use == for string content!

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 compareToIgnoreCase with Turkish strings can produce wrong results. Use a locale-aware Collator instead.
  • 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 with java.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

Before sort "orange" "Apple" "banana" compareTo() After sort "Apple" "banana" "orange"

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

Clean string s t a t u s length = 6 compareTo result: 0 ✓ "status" String with trailing space! s t a t u s SP U+0020 length = 7 (invisible!) compareTo result: non-zero ✗ "status " Fix: call .strip() before comparing — Use DiffChecker to spot invisible characters visually

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

Language Method Return type Case-insensitive Java compareTo() int compareToIgnoreCase() Kotlin compareTo() Int compareTo(ignoreCase=true) C# CompareTo() int string.Compare(OrdinalIC) Python < / > / == operators bool / bool .lower() then compare JavaScript localeCompare() -1 / 0 / 1 localeCompare(sensitivity:'base') All languages use the sign convention: negative / zero / positive

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 (unlike equals()). 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 on intern() calls), so it is only worthwhile for very large pools of repeated strings.
  • Collator caching: Collator.getInstance() is relatively expensive. Cache the Collator instance rather than constructing it on every comparison call.
  • Avoid toLowerCase() + equals(): This pattern allocates a new string on every call. Use equalsIgnoreCase() or compareToIgnoreCase() 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