Java String Methods
A String in Java isn’t just a sequence of characters — it’s a full object with dozens of built-in methods for searching, transforming, comparing, and rebuilding text. Because String objects are immutable, every method that looks like it “changes” a string actually returns a brand-new String and leaves the original untouched. Nearly every Java program that touches user input, files, or text processing leans on these methods constantly, so knowing them well — and knowing their gotchas — pays off immediately. This lesson covers the core String API from the ground up, how it behaves internally, and the mistakes that trip up even experienced developers.
Overview: How String Methods Work
In Java, String is a final class (you cannot extend it) backed internally by a character array — technically a byte[] since Java 9’s “compact strings” optimization, which stores Latin-1 text using one byte per character instead of two, falling back to UTF-16 only when needed. That backing array is never exposed and never modified after construction: String is immutable by design. This has several consequences you’ll feel constantly:
- Every method that appears to modify a string (
toUpperCase(),trim(),replace(),substring(), etc.) actually allocates and returns a newStringobject. The original is untouched. - String literals (like
"Java") are stored in a special region of the heap called the string pool. When the compiler sees the same literal used twice, both references point to the exact same object — this is called interning. Strings built at runtime withnew String(...)or by concatenating variables are not automatically pooled, which is why comparing them with==is unreliable (more on this in Common Mistakes). - Immutability makes strings inherently thread-safe (no locking needed to read them) and allows the JVM to safely cache a string’s hash code the first time
hashCode()is called, since it can never change. - When you write
str1 + str2in source code, the compiler doesn’t call any String method directly — it desugars the concatenation into calls to a mutable buffer (historicallyStringBuilder.append(), and since Java 9 often aninvokedynamiccall toStringConcatFactory) which builds the combined characters once and produces a single newStringat the end.
Because every transformation allocates a new object, chaining many String method calls in a loop (e.g. repeatedly using += to build a large string) creates a lot of short-lived garbage. That’s exactly the problem the mutable StringBuilder class solves, which you’ll see used later in this lesson.
Syntax
String methods are called using dot notation on a string reference: stringReference.methodName(arguments). Since methods return new String objects (not void), calls are commonly chained: text.trim().toLowerCase().replace("a", "b"). The table below lists the methods you’ll use most often.
| Method | Returns | Description |
|---|---|---|
length() |
int |
Number of characters in the string |
charAt(int index) |
char |
Character at the given zero-based index |
substring(int begin) |
String |
Everything from begin to the end |
substring(int begin, int end) |
String |
Characters from begin (inclusive) to end (exclusive) |
indexOf(String s) / lastIndexOf(String s) |
int |
Position of first/last occurrence, or -1 if not found |
contains(CharSequence s) |
boolean |
Whether the substring appears anywhere |
toUpperCase() / toLowerCase() |
String |
New string with case changed |
trim() / strip() |
String |
New string with leading/trailing whitespace removed (strip() is Unicode-aware, added in Java 11) |
replace(CharSequence a, CharSequence b) |
String |
Replaces every literal occurrence of a with b |
replaceAll(String regex, String repl) |
String |
Replaces every regex match |
split(String regex) |
String[] |
Splits the string by a regex delimiter |
equals(Object o) / equalsIgnoreCase(String s) |
boolean |
Compares string content, not object identity |
compareTo(String s) |
int |
Lexicographic comparison (negative, zero, or positive) |
startsWith(String s) / endsWith(String s) |
boolean |
Prefix/suffix check |
isEmpty() / isBlank() |
boolean |
True if length is 0 / true if empty or only whitespace (Java 11+) |
String.join(CharSequence delim, CharSequence... elements) |
String |
Static method that joins pieces with a delimiter |
Examples
Example 1: Inspecting and transforming a string
public class Main {
public static void main(String[] args) {
String greeting = " Hello, Java World! ";
System.out.println("Length: " + greeting.length());
System.out.println("Trimmed: [" + greeting.trim() + "]");
System.out.println("Upper: " + greeting.toUpperCase());
System.out.println("Lower: " + greeting.toLowerCase());
String trimmed = greeting.trim();
System.out.println("Char at 0: " + trimmed.charAt(0));
System.out.println("Index of 'Java': " + trimmed.indexOf("Java"));
System.out.println("Substring(7): " + trimmed.substring(7));
System.out.println("Substring(7, 11): " + trimmed.substring(7, 11));
System.out.println("Contains 'World': " + trimmed.contains("World"));
System.out.println("Replace: " + trimmed.replace("Java", "Kotlin"));
}
}
Output:
Length: 22
Trimmed: [Hello, Java World!]
Upper: HELLO, JAVA WORLD!
Lower: hello, java world!
Char at 0: H
Index of 'Java': 7
Substring(7): Java World!
Substring(7, 11): Java
Contains 'World': true
Replace: Hello, Kotlin World!
Notice that greeting itself is never modified — trim(), toUpperCase(), and toLowerCase() each return a new string, so you must capture the result (as trimmed does) to use it later. Also note that substring(7, 11) includes index 7 but stops before index 11 — a common source of off-by-one bugs.
Example 2: Cleaning and formatting real user input
public class Main {
public static void main(String[] args) {
String rawName = " jOHN smiTH ";
String cleaned = rawName.trim().replaceAll("\\s+", " ");
System.out.println("Cleaned: [" + cleaned + "]");
String[] parts = cleaned.split(" ");
StringBuilder formatted = new StringBuilder();
for (String part : parts) {
String lower = part.toLowerCase();
String capitalized = lower.substring(0, 1).toUpperCase() + lower.substring(1);
formatted.append(capitalized).append(" ");
}
String result = formatted.toString().trim();
System.out.println("Formatted name: " + result);
String email = "JOHN.SMITH@example.com";
System.out.println("Email lowercase: " + email.toLowerCase());
System.out.println("Starts with 'john': " + email.toLowerCase().startsWith("john"));
System.out.println("Ends with '.com': " + email.endsWith(".com"));
System.out.println("Is blank check: " + " ".isBlank());
}
}
Output:
Cleaned: [jOHN smiTH]
Formatted name: John Smith
Email lowercase: john.smith@example.com
Starts with 'john': true
Ends with '.com': true
Is blank check: true
This example mirrors real form-input cleanup: replaceAll("\\s+", " ") collapses any run of whitespace (tabs, multiple spaces) into a single space, split(" ") breaks the name into words, and each word is re-capitalized manually since Java has no built-in “title case” method. Because none of these methods mutate their receiver, the code reassigns the result at every step.
Example 3: Comparison, StringBuilder, and splitting on delimiters
public class Main {
public static void main(String[] args) {
String a = "Java";
String b = "Java";
String c = new String("Java");
System.out.println("a == b: " + (a == b));
System.out.println("a == c: " + (a == c));
System.out.println("a.equals(c): " + a.equals(c));
System.out.println("a.equalsIgnoreCase(\"JAVA\"): " + a.equalsIgnoreCase("JAVA"));
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append(i);
if (i < 5) {
sb.append(", ");
}
}
System.out.println("Built string: " + sb.toString());
System.out.println("Reversed: " + sb.reverse().toString());
String csv = "apple,banana,,cherry";
String[] fruits = csv.split(",");
System.out.println("Number of parts: " + fruits.length);
System.out.println("Joined with ' | ': " + String.join(" | ", fruits));
}
}
Output:
a == b: true
a == c: false
a.equals(c): true
a.equalsIgnoreCase("JAVA"): true
Built string: 1, 2, 3, 4, 5
Reversed: 5 ,4 ,3 ,2 ,1
Number of parts: 4
Joined with ' | ': apple | banana | | cherry
a and b are the same pooled literal object, so == reports true. c was created with new String(...), forcing a distinct heap object, so == reports false even though the content is identical — equals() is what correctly reports true. The StringBuilder loop shows why you use a mutable buffer instead of repeated String concatenation inside loops. Finally, notice that split(",") on "apple,banana,,cherry" keeps the empty string between the two commas (giving 4 parts, not 3) — split only drops trailing empty strings by default.
Under the Hood: What Happens When You Call a String Method
- Literals and the pool: When the class loads, the JVM sees the literal
"Java"and either adds it to the string pool or reuses an existing entry. Any other occurrence of the identical literal in your compiled code reuses that same reference. - Calling a transforming method: A call like
trimmed.substring(7)does not touchtrimmed's backing array. Instead, the JVM allocates a new byte/char array sized for the result, copies the relevant range of characters into it, and wraps that array in a brand-newStringobject, which is what gets returned. - Concatenation with
+: The compiler rewrites chains of+into eitherStringBuildercalls or aninvokedynamiccall to the JDK's string-concatenation bootstrap method, which computes the final length once and writes all pieces into a single buffer — far more efficient than N intermediateStringallocations. StringBuilderinternals: UnlikeString,StringBuilderwraps a mutable, resizable character array.append()writes directly into that buffer; if the buffer runs out of room, the JVM allocates a larger array (typically doubling capacity) and copies the existing data over — the same growth strategy used byArrayList.- Garbage collection: Every intermediate
Stringproduced by chained method calls becomes garbage as soon as nothing references it, and the JVM's garbage collector reclaims that memory later. This is harmless for occasional use but adds real overhead inside hot loops.
Common Mistakes
Mistake 1: Comparing strings with == instead of equals()
== compares object references, not string content. It happens to "work" for two literals because both point at the same pooled object, which lulls beginners into a false sense of safety — it silently breaks the moment a string comes from user input, concatenation, or new String(...).
public class Main {
public static void main(String[] args) {
String input = new String("yes");
if (input == "yes") {
System.out.println("Matched");
} else {
System.out.println("No match (unexpected!)");
}
if (input.equals("yes")) {
System.out.println("Matched with equals()");
}
}
}
Output:
No match (unexpected!)
Matched with equals()
Fix: Always use .equals() (or .equalsIgnoreCase()) for content comparison. Reserve == for checking whether two references point to the exact same object, which is rarely what you actually want with strings.
Mistake 2: Forgetting that split() takes a regular expression
split(String) treats its argument as a regex, not a literal string. Characters like ., *, |, and + have special regex meaning, so splitting on "." doesn't split on the dot character — it splits on every character, because . matches any single character in regex.
public class Main {
public static void main(String[] args) {
String path = "file.name.txt";
String[] wrongParts = path.split(".");
System.out.println("Wrong split length: " + wrongParts.length);
String[] correctParts = path.split("\\.");
System.out.println("Correct split length: " + correctParts.length);
for (String part : correctParts) {
System.out.println(" - " + part);
}
}
}
Output:
Wrong split length: 0
Correct split length: 3
- file
- name
- txt
Fix: Escape regex metacharacters with a backslash (written as "\\." in source code) or use Pattern.quote(".") when the delimiter comes from a variable and might contain special characters.
Best Practices
- Always compare string content with
equals()orequalsIgnoreCase(), never==. - Use
StringBuilderinstead of repeated+=concatenation inside loops to avoid creating excessive short-lived objects. - Prefer
isEmpty()orisBlank()over comparinglength() == 0or comparing against""— they're clearer and handle whitespace-only strings correctly. - Remember that
split(),replaceAll(), andmatches()take regular expressions; usereplace()(notreplaceAll()) when you just need a literal substring swap. - Check
indexOf()results against-1before using them insubstring()to avoid aStringIndexOutOfBoundsException. - Be cautious with locale-sensitive methods like
toUpperCase()in internationalized code — use thetoUpperCase(Locale)overload when the default locale could produce surprising results (e.g. Turkish 'i'). - Never rely on string interning for correctness; treat it strictly as a JVM memory optimization you don't control.
Practice Exercises
- Exercise 1: Write a program that takes the string
"programming in java is fun"and prints it with every word capitalized ("Programming In Java Is Fun"), using onlysplit(),substring(), andtoUpperCase()/toLowerCase(). - Exercise 2: Given the string
"racecar", write a program that checks whether it's a palindrome (reads the same forwards and backwards) usingcharAt()in a loop — do not useStringBuilder.reverse(). - Exercise 3: Given a comma-separated string of numbers like
"4,8,15,16,23,42", split it, convert each piece to anintwithInteger.parseInt(), and print their sum. Expected output:108.
Summary
Stringobjects are immutable — every transforming method returns a newStringrather than modifying the original.- String literals are pooled and interned by the JVM, which is why
==can appear to "work" on literals but fails on strings built at runtime; always useequals()for content comparison. - Core methods to know cold:
length(),charAt(),substring(),indexOf(),contains(),toUpperCase()/toLowerCase(),trim()/strip(),replace()/replaceAll(),split(), andequals()/equalsIgnoreCase(). - Use
StringBuilderfor building strings incrementally, especially inside loops, since it uses a mutable, resizable buffer instead of allocating a new object per step. split()andreplaceAll()take regular expressions — escape metacharacters like.when you mean them literally.
