Skip to content
Java strings 5 min read

String Comparison

Comparing strings is one of the most common operations you’ll write in Java, and it’s also one of the most common sources of hard-to-spot bugs. Java gives you several ways to compare strings, and choosing the right one depends on whether you care about identity, content, or alphabetical order.

The == Trap — Reference vs Value

The single biggest mistake beginners make is using == to compare strings. In Java, == checks reference equality — it asks “are these two variables pointing to the exact same object in memory?” — not whether the text content is the same.

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b);      // true  (both from the string pool)
System.out.println(a == c);      // false (c is a brand-new heap object)
System.out.println(a.equals(c)); // true  (same content)

Output:

true
false
true

a and b both point to the same interned literal in the String Pool, so == returns true. But c was created with new String(...), so it lives on the heap as a separate object — different reference, same content.

Warning: Never use == to compare string content. It works by accident with string literals but will silently break when strings come from user input, files, APIs, or new String(...).

equals() — Content Comparison

equals() is the standard, correct way to compare two strings for identical content. It is case-sensitive.

String name1 = "Java";
String name2 = "Java";
String name3 = "java";

System.out.println(name1.equals(name2)); // true
System.out.println(name1.equals(name3)); // false — 'J' != 'j'

Output:

true
false

Tip: When comparing against a known literal, put the literal first to avoid a NullPointerException if the variable is null: "expected".equals(userInput) is safer than userInput.equals("expected").

equalsIgnoreCase() — Case-Insensitive Comparison

When case doesn’t matter (e.g., checking a command, a username, or a file extension), use equalsIgnoreCase().

String input  = "YES";
String answer = "yes";

if (input.equalsIgnoreCase(answer)) {
    System.out.println("They match, ignoring case!");
}

Output:

They match, ignoring case!

Internally, this method compares characters one-by-one after calling Character.toLowerCase() on each, so it handles ASCII case differences correctly. For full Unicode locale-aware comparison, look at java.text.Collator.

compareTo() — Lexicographic (Alphabetical) Ordering

compareTo() returns an integer rather than a boolean:

Return valueMeaning
negativeCalling string comes before the argument alphabetically
zeroBoth strings are equal
positiveCalling string comes after the argument alphabetically
String s1 = "Apple";
String s2 = "Banana";
String s3 = "Apple";

System.out.println(s1.compareTo(s2));  // negative (A < B)
System.out.println(s2.compareTo(s1));  // positive (B > A)
System.out.println(s1.compareTo(s3));  // 0        (equal)

Output:

-1
1
0

Note: compareTo() is case-sensitive. "apple".compareTo("Apple") returns a positive number because lowercase 'a' (97) has a higher Unicode code point than uppercase 'A' (65).

compareToIgnoreCase()

Behaves like compareTo() but ignores case, making it ideal for sorting user-visible text.

String[] fruits = {"banana", "Apple", "cherry"};
java.util.Arrays.sort(fruits, String::compareToIgnoreCase);
System.out.println(java.util.Arrays.toString(fruits));

Output:

[Apple, banana, cherry]

contains(), startsWith(), endsWith() — Partial Matching

Sometimes you don’t need an exact comparison — you want to know whether a string contains a substring or begins/ends with a particular sequence.

String url = "https://devcraftly.com/java";

System.out.println(url.startsWith("https"));   // true
System.out.println(url.endsWith("/java"));      // true
System.out.println(url.contains("devcraftly")); // true
System.out.println(url.contains("python"));     // false

Output:

true
true
true
false

These methods don’t throw if the string is empty — an empty string "" is considered to be contained in (and to start/end) every string.

Objects.equals() — Null-Safe Comparison

When either string could be null, wrapping the check in Objects.equals() prevents a NullPointerException:

import java.util.Objects;

String a = null;
String b = "hello";

System.out.println(Objects.equals(a, b)); // false — no NPE
System.out.println(Objects.equals(a, null)); // true — both null

Output:

false
true

Tip: Prefer Objects.equals() over null-guard boilerplate like (a != null && a.equals(b)). It’s cleaner and handles the symmetric case where both are null.

Quick Reference Table

MethodCase-sensitiveReturnsNull-safeUse when…
==booleanyes (no NPE)Never for content
equals()yesbooleannoExact content match
equalsIgnoreCase()nobooleannoCase-insensitive match
compareTo()yesintnoSorting / ordering
compareToIgnoreCase()nointnoCase-insensitive sort
contains()yesbooleannoSubstring check
startsWith()yesbooleannoPrefix check
endsWith()yesbooleannoSuffix check
Objects.equals()yesbooleanyesNull-safe equals

Under the Hood

How equals() is Implemented

String.equals() in the JDK first checks reference equality (this == obj) as a fast path — if the same reference, they’re trivially equal. Then it compares lengths; if they differ, the strings can’t be equal. Finally, it compares the underlying byte arrays character by character. Since Java 9, strings are stored as byte[] with a coder field (Latin-1 for ASCII-range content, UTF-16 otherwise), so the comparison is optimized for the common case.

Why == Works for Literals

String literals are automatically interned into the String Pool at compile time. When the class is loaded, the JVM checks whether an identical string already lives in the pool; if so, the new variable just points to the existing object. That’s why "hello" == "hello" is reliably true for literals. But any string produced at runtime (user input, new String(...), StringBuilder.toString(), etc.) lives outside the pool, so == becomes unreliable.

compareTo() Arithmetic

String.compareTo() returns this.charAt(k) - anotherString.charAt(k) at the first differing index k, or this.length() - anotherString.length() if one is a prefix of the other. The sign of this difference directly encodes the ordering, which is why you see values like -1, 0, or 32 (the distance between 'a' and 'A' in Unicode) rather than always exactly 1.

  • Strings — the full overview of the String class and how strings work in Java
  • String Pool & intern() — deep-dive into why == can fool you and how string interning works
  • String Immutability — understand why strings can’t be changed and how that affects comparisons
  • String Methods — the full catalogue of built-in String methods, including search and transformation
  • Comparable — implement natural ordering for your own classes, similar to compareTo()
  • Regular Expressions — pattern-based string matching when simple comparison isn’t enough
Last updated June 13, 2026
Was this helpful?