☕ Java
String Comparison
Comparing strings in Java requires choosing the right method for the right purpose. Java provides several ways to compare strings: equals() for exact content equality, equalsIgnoreCase() for case-insensitive equality, compareTo() for lexicographic ordering, compareToIgnoreCase() for case-insensitive ordering, contains() and startsWith() for substring checks, and matches() for regex-based comparison. The fundamental rule is that == compares references, not content — using == to compare string values is one of the most common bugs in Java code.
Why String Comparison Is Different from Primitive Comparison
For primitive types like int and double, the == operator compares values directly. Writing if (x == 5) checks whether the integer variable x holds the value 5. This works correctly because primitives are values stored directly in variables.
Strings in Java are objects. A String variable holds a reference — a memory address pointing to a String object on the heap. When you write if (name == "Alice"), you are comparing two references — asking whether both sides point to the same object in memory, not whether they contain the same sequence of characters. Two different String objects can contain identical characters while being at different memory addresses, making == return false even though the content is identical.
This is not a quirk or a bug — it is the correct and consistent behaviour of reference types in Java. The == operator always compares references for objects. The problem arises only when developers incorrectly use == expecting it to compare content. The solution is to always use the equals() method for string content comparison, which compares character by character and returns true when the content is identical regardless of whether the two String variables point to the same object.
Understanding this distinction is particularly important because Java's string interning can sometimes make == appear to work correctly, creating false confidence. When string literals appear in source code, the JVM may store them in a shared pool and return the same reference for identical literals. This makes name == "Alice" return true when name was assigned "Alice" directly from a literal. But name does not equal "Alice" when it comes from user input, a file, a database, or String construction — because those strings are separate objects not in the pool. Relying on == produces code that appears to work during testing but fails in production.
Java
// ── The == problem with strings: ─────────────────────────────────────
String a = "Hello";
String b = "Hello";
String c = new String("Hello"); // explicitly new object
// == compares references (memory addresses):
System.out.println(a == b); // true — may share pool reference
System.out.println(a == c); // false — c is a separate object
System.out.println(b == c); // false — c is a separate object
// .equals() compares content:
System.out.println(a.equals(b)); // true — same content
System.out.println(a.equals(c)); // true — same content
System.out.println(b.equals(c)); // true — same content
// ── The dangerous false-positive from string interning: ───────────────
String literal1 = "Java";
String literal2 = "Java";
System.out.println(literal1 == literal2); // true — same pool reference
// This appears to work, but ONLY because both are compile-time literals.
// It will fail when strings come from user input, files, or APIs.
// ── Strings from user input — == fails: ───────────────────────────────
Scanner scanner = new Scanner(System.in);
System.out.print("Enter: ");
String input = scanner.nextLine(); // "Java" typed by user
System.out.println(input == "Java"); // false — different objects!
System.out.println(input.equals("Java")); // true — same content
// ── Always use equals() for content comparison: ───────────────────────
String username = getUsernameFromDatabase(); // comes from DB
if (username.equals("admin")) { // CORRECT
grantAdminAccess();
}
// if (username == "admin") — WRONG — almost never works as expectedString Comparison Methods
Java's String class provides a rich set of comparison methods covering every common comparison need. equals() and equalsIgnoreCase() are the primary tools for equality checks. compareTo() and compareToIgnoreCase() provide lexicographic ordering — they return a negative number if the receiver comes before the argument, zero if they are equal, and a positive number if the receiver comes after. This three-way result makes them suitable for sorting and ordering operations.
The startsWith(), endsWith(), and contains() methods check substring relationships. regionMatches() compares a specific region within a string. matches() applies a regular expression. All of these methods operate on content, not references, and are safe to use with any String regardless of how it was created.
A subtle and important pitfall is calling an instance method on a potentially null String variable. If username is null and you write username.equals("admin"), the result is a NullPointerException — calling any method on a null reference throws. The safe idiom is to place the literal on the left side: "admin".equals(username). If username is null, the result is false (the literal "admin" is not null and its equals() method handles null correctly), and no exception is thrown. This null-safe form should be used whenever the string variable might be null.
For case-insensitive comparisons, equalsIgnoreCase() is more readable and correct than manually converting both sides to uppercase or lowercase before comparing. The manual conversion approach can fail for some Unicode characters where toUpperCase() and toLowerCase() are not symmetric or locale-dependent.
Java
String str = "Hello World";
// ── equals() — exact content match: ──────────────────────────────────
System.out.println(str.equals("Hello World")); // true
System.out.println(str.equals("hello world")); // false — case matters
System.out.println(str.equals("Hello")); // false — length differs
System.out.println(str.equals(null)); // false — safe, no NPE
// ── equalsIgnoreCase() — case-insensitive match: ──────────────────────
System.out.println(str.equalsIgnoreCase("HELLO WORLD")); // true
System.out.println(str.equalsIgnoreCase("hello world")); // true
System.out.println(str.equalsIgnoreCase("Hello")); // false
// ── Null-safe idiom — literal on the left: ───────────────────────────
String username = null;
// username.equals("admin") — NullPointerException!
System.out.println("admin".equals(username)); // false — safe
System.out.println("admin".equalsIgnoreCase(username)); // false — safe
// ── compareTo() — lexicographic ordering (returns int): ───────────────
String s1 = "Apple";
String s2 = "Banana";
String s3 = "Apple";
System.out.println(s1.compareTo(s2)); // negative — Apple < Banana
System.out.println(s2.compareTo(s1)); // positive — Banana > Apple
System.out.println(s1.compareTo(s3)); // 0 — identical content
// Result is: first different char's Unicode difference (or length difference)
System.out.println("abc".compareTo("abd")); // -1 (c=99, d=100: 99-100)
System.out.println("abc".compareTo("ab")); // 1 (length: 3-2)
System.out.println("ABC".compareTo("abc")); // -32 (A=65, a=97: 65-97)
// ── compareToIgnoreCase(): ────────────────────────────────────────────
System.out.println("Apple".compareToIgnoreCase("apple")); // 0
System.out.println("apple".compareToIgnoreCase("Banana")); // negative
// ── Substring checks: ─────────────────────────────────────────────────
String text = "The quick brown fox";
System.out.println(text.startsWith("The")); // true
System.out.println(text.startsWith("quick", 4)); // true — offset 4
System.out.println(text.endsWith("fox")); // true
System.out.println(text.contains("brown")); // true
System.out.println(text.contains("cat")); // false
// ── Sorting with compareTo: ───────────────────────────────────────────
String[] names = {"Charlie", "Alice", "Bob", "Diana"};
Arrays.sort(names); // uses compareTo() internally
System.out.println(Arrays.toString(names)); // [Alice, Bob, Charlie, Diana]String Comparison for Sorting and Ordering
The compareTo() method is the foundation of string sorting in Java. When you call Arrays.sort() or Collections.sort() on a list of strings, the sort algorithm calls compareTo() to determine the relative order of pairs of strings. Understanding what compareTo() returns — negative, zero, or positive — is necessary for implementing custom sort orders and for working with TreeMap and TreeSet which maintain sorted order.
Lexicographic ordering is not the same as alphabetical ordering for all string content. Lexicographic ordering is based on Unicode code points, which means uppercase letters come before lowercase letters, digits come before letters, and characters with accents or diacritics may sort differently than expected. For human-readable sorting that handles these cases correctly, Java's Collator class (which takes locale into account) is more appropriate than simple compareTo().
Custom sorting with Comparator.comparing() uses compareTo() under the hood. The idiomatic Java way to sort strings by multiple criteria — first by length, then alphabetically for strings of the same length — is to chain Comparator instances. This produces clean, readable sort specifications that can be passed to sort methods.
Java
// ── Natural ordering with Arrays.sort: ───────────────────────────────
String[] fruits = {"Mango", "apple", "Banana", "cherry"};
Arrays.sort(fruits);
System.out.println(Arrays.toString(fruits));
// [Banana, Mango, apple, cherry]
// Uppercase letters sort before lowercase in Unicode:
// B(66), M(77), a(97), c(99) — so Banana < Mango < apple < cherry
// ── Case-insensitive sort: ────────────────────────────────────────────
Arrays.sort(fruits, String.CASE_INSENSITIVE_ORDER);
System.out.println(Arrays.toString(fruits));
// [apple, Banana, cherry, Mango]
// ── Custom Comparator — sort by length, then alphabetically: ──────────
String[] words = {"cat", "elephant", "dog", "ant", "rhinoceros", "ox"};
Arrays.sort(words, Comparator
.comparingInt(String::length) // primary: sort by length
.thenComparing(Comparator.naturalOrder()) // secondary: alphabetical
);
System.out.println(Arrays.toString(words));
// [ox, ant, cat, dog, elephant, rhinoceros]
// ── Locale-aware sorting with Collator: ──────────────────────────────
Collator collator = Collator.getInstance(Locale.UK);
String[] german = {"Ärger", "Apfel", "Zeitung", "über"};
Arrays.sort(german, collator); // correctly handles accented characters
System.out.println(Arrays.toString(german));
// ── Manual binary search with compareTo: ──────────────────────────────
public static int binarySearch(String[] sorted, String target) {
int low = 0, high = sorted.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
int cmp = sorted[mid].compareTo(target);
if (cmp < 0) low = mid + 1;
else if (cmp > 0) high = mid - 1;
else return mid; // found
}
return -(low + 1); // not found — insertion point
}
String[] sorted = {"Alice", "Bob", "Charlie", "Diana", "Eve"};
System.out.println(binarySearch(sorted, "Charlie")); // 2
System.out.println(binarySearch(sorted, "Frank")); // negativeRelated Topics in Strings
String Class
String is one of the most fundamental classes in Java — used in virtually every program, yet deeply misunderstood by many developers. A String represents an immutable sequence of Unicode characters. It is not a primitive type but a full class in java.lang, automatically imported into every Java file. Understanding String means understanding how it is stored in memory, why it is immutable, how the string pool works, what the difference between == and equals() means for strings, and how to use the class efficiently. This entry covers String's nature as a class, its internal representation, the critical distinction between reference equality and value equality, String's place in the type hierarchy, and the design decisions that make String behave the way it does.
String Pool
The string pool (also called the string intern pool or string constant pool) is a special memory region maintained by the JVM that stores a single copy of each unique string value. When two string literals have the same content, they refer to the same object in the pool rather than two separate objects. The pool is a flyweight pattern applied at the language level — it dramatically reduces memory consumption in applications that use many repeated string values, which is nearly every application. This entry covers how the pool works, where it lives in JVM memory, how to interact with it programmatically, the intern() method, performance implications, and when to use or avoid pool entries.
Immutable String
String immutability is the most important design decision in Java's String class. Once a String object is created, its character sequence can never change. No method on String modifies the string; every method that appears to modify returns a new String object containing the result. This design decision drives thread safety, enables the string pool, makes strings safe hash map keys, and simplifies reasoning about string values. Understanding why String is immutable, how immutability is enforced, and what the consequences of immutability are clarifies the behaviour of virtually every piece of Java code that handles strings.
Mutable String
Java provides two mutable string classes for scenarios where String's immutability would be inefficient: StringBuilder and StringBuffer. Both maintain an internal character buffer that can be modified in place — characters can be appended, inserted, deleted, and replaced without creating new objects. StringBuilder is the modern choice for single-threaded use; StringBuffer is the legacy thread-safe version with synchronised methods. This entry covers the internal buffer mechanics, the full API of both classes, performance characteristics, when to use each, thread safety implications, and the complete patterns for efficient string construction.