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, ornew 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
NullPointerExceptionif the variable isnull:"expected".equals(userInput)is safer thanuserInput.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 value | Meaning |
|---|---|
| negative | Calling string comes before the argument alphabetically |
| zero | Both strings are equal |
| positive | Calling 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
| Method | Case-sensitive | Returns | Null-safe | Use when… |
|---|---|---|---|---|
== | — | boolean | yes (no NPE) | Never for content |
equals() | yes | boolean | no | Exact content match |
equalsIgnoreCase() | no | boolean | no | Case-insensitive match |
compareTo() | yes | int | no | Sorting / ordering |
compareToIgnoreCase() | no | int | no | Case-insensitive sort |
contains() | yes | boolean | no | Substring check |
startsWith() | yes | boolean | no | Prefix check |
endsWith() | yes | boolean | no | Suffix check |
Objects.equals() | yes | boolean | yes | Null-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.
Related Topics
- 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