String Basics and Immutability
Strings are one of the most frequently used data types in Java programming. Understanding how strings work internally, especially the concept of immutability, is crucial for writing efficient code and acing coding interviews. In Java, strings are objects that represent sequences of characters, and once created, they cannot be changed.
What is a String?
In Java, a String is an object that represents a sequence of characters. Unlike primitive data types like
int or char, String is a class in the java.lang package. Every string
literal you create (text in double quotes) is actually an instance of the String class.
String Immutability
Immutability means that once a String object is created, its content cannot be changed. Any operation that appears to modify a string (like concatenation or replacement) actually creates a new String object with the modified content, leaving the original unchanged.
Why immutable? Security (strings are used in network connections, file paths, database queries), thread safety (multiple threads can share strings safely), and caching (the String pool optimization).
Creating Strings
There are two main ways to create strings in Java. Understanding the difference is crucial because it affects memory usage and equality comparisons.
// Method 1: String Literal (recommended)
String greeting = "Hello, World!";
String name = "Alice";
// Method 2: Using 'new' keyword (creates new object)
String greeting2 = new String("Hello, World!");
// Both methods create valid strings
System.out.println(greeting); // Hello, World!
System.out.println(greeting2); // Hello, World!
new keyword.
The String Pool
Java maintains a special memory area called the String Pool (or String Intern Pool) to optimize memory usage. When you create a string literal, Java first checks if an identical string already exists in the pool. If it does, the new variable simply points to the existing string instead of creating a duplicate.
| Creation Method | String Pool? | Memory Location |
|---|---|---|
String s = "hello" |
Yes | String Pool (Heap) |
String s = new String("hello") |
No | Regular Heap Memory |
String s = new String("hello").intern() |
Yes | String Pool (Heap) |
This example shows how String Pool affects reference equality:
// String Pool demonstration
String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
// s1 and s2 point to the SAME object in String Pool
System.out.println(s1 == s2); // true (same reference)
// s3 is a NEW object in heap, different from pool
System.out.println(s1 == s3); // false (different references)
// But content is the same
System.out.println(s1.equals(s3)); // true (same content)
Immutability in Action
Let's see what really happens when we "modify" a string. Each operation creates a new String object while the original remains unchanged.
String original = "Hello";
System.out.println("Original: " + original); // Hello
System.out.println("Hashcode: " + original.hashCode()); // 69609650
// These operations create NEW strings
String upper = original.toUpperCase();
String concat = original + " World";
String replaced = original.replace('l', 'x');
// Original is UNCHANGED
System.out.println("Original after ops: " + original); // Hello
System.out.println("Same hashcode: " + original.hashCode()); // 69609650
// New strings have different references
System.out.println("Uppercase: " + upper); // HELLO
System.out.println("Concatenated: " + concat); // Hello World
System.out.println("Replaced: " + replaced); // Hexxo
String Length and Character Access
Every string knows its length and allows you to access individual characters by their index position. Like arrays, string indices start at 0.
String text = "Java";
// Get length (number of characters)
int len = text.length(); // 4
// Access individual characters by index
char first = text.charAt(0); // 'J'
char second = text.charAt(1); // 'a'
char last = text.charAt(3); // 'a'
// Using length() to get last character
char lastChar = text.charAt(text.length() - 1); // 'a'
// Convert to char array
char[] chars = text.toCharArray(); // {'J', 'a', 'v', 'a'}
// Iterate through characters
for (int i = 0; i < text.length(); i++) {
System.out.print(text.charAt(i) + " "); // J a v a
}
// Enhanced for loop with char array
for (char c : text.toCharArray()) {
System.out.print(c + " "); // J a v a
}
StringIndexOutOfBoundsException. Always validate indices before accessing characters.
Practice Questions: String Basics
Task: Write a method that counts and returns the number of characters in a string (excluding spaces).
Show Solution
int countNonSpaceChars(String str) {
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) != ' ') {
count++;
}
}
return count;
}
// Test: countNonSpaceChars("Hello World") returns 10
Task: Write a method that returns a string containing only the first and last character of the input.
Show Solution
String getFirstAndLast(String str) {
if (str == null || str.isEmpty()) {
return "";
}
if (str.length() == 1) {
return str + str; // Same char twice
}
char first = str.charAt(0);
char last = str.charAt(str.length() - 1);
return "" + first + last;
}
// Test: getFirstAndLast("Hello") returns "Ho"
Task: Given the code below, predict the output and explain why.
String a = "test";
String b = "test";
String c = new String("test");
String d = c.intern();
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == d);
Show Solution
// Output:
// true - a and b both point to "test" in String Pool
// false - c is a new object in heap, not in pool
// true - intern() returns pool reference, which is same as a
// Key insight: intern() checks if the string exists in
// the pool. If yes, returns pool reference. If no, adds
// it to pool and returns that reference.
Task: Reverse a string by iterating through characters (without using StringBuilder.reverse()).
Show Solution
String reverseString(String str) {
String reversed = "";
for (int i = str.length() - 1; i >= 0; i--) {
reversed += str.charAt(i);
}
return reversed;
}
// Test: reverseString("Hello") returns "olleH"
// Note: This is O(n^2) due to string concatenation!
// Better approach uses StringBuilder (covered later)
Essential String Methods
Java's String class provides a rich set of methods for manipulating and querying strings. These methods are your primary tools for solving string-related problems in coding interviews. From extracting substrings to searching for patterns, mastering these methods will significantly speed up your problem-solving.
Substring Extraction
The substring() method extracts a portion of a string. There are two versions: one that takes a
start index, and another that takes both start and end indices. Remember that the end index is exclusive.
String text = "Hello, World!";
// substring(beginIndex) - from index to end
String fromIndex5 = text.substring(5); // ", World!"
String fromIndex7 = text.substring(7); // "World!"
// substring(beginIndex, endIndex) - endIndex is EXCLUSIVE
String hello = text.substring(0, 5); // "Hello"
String world = text.substring(7, 12); // "World"
// Common pattern: extract last N characters
String str = "filename.txt";
String lastFour = str.substring(str.length() - 4); // ".txt"
// Extract file extension
int dotIndex = str.lastIndexOf('.');
String extension = str.substring(dotIndex + 1); // "txt"
substring(start, end) includes characters from index start
up to but NOT including index end. The length of result = end - start.
Searching in Strings
Finding characters or substrings within a string is a common task. Java provides several methods for this purpose, each returning -1 if the search target is not found.
String text = "Hello, World! Hello, Java!";
// indexOf - find FIRST occurrence
int firstHello = text.indexOf("Hello"); // 0
int firstO = text.indexOf('o'); // 4
int notFound = text.indexOf("Python"); // -1
// indexOf with start position
int secondHello = text.indexOf("Hello", 1); // 14
// lastIndexOf - find LAST occurrence
int lastO = text.lastIndexOf('o'); // 22
int lastHello = text.lastIndexOf("Hello"); // 14
// contains - check if substring exists (returns boolean)
boolean hasWorld = text.contains("World"); // true
boolean hasPython = text.contains("Python"); // false
// startsWith and endsWith
boolean startsWithHello = text.startsWith("Hello"); // true
boolean endsWithJava = text.endsWith("Java!"); // true
// Check if string starts with prefix at specific index
boolean worldAt7 = text.startsWith("World", 7); // true
Case Conversion
Converting between uppercase and lowercase is straightforward with dedicated methods. These are particularly useful for case-insensitive comparisons.
String mixed = "Hello World";
// Convert entire string
String upper = mixed.toUpperCase(); // "HELLO WORLD"
String lower = mixed.toLowerCase(); // "hello world"
// Case-insensitive comparison (common pattern)
String input = "YES";
boolean isYes = input.toLowerCase().equals("yes"); // true
// Or use equalsIgnoreCase
boolean isYes2 = input.equalsIgnoreCase("yes"); // true
Removing Whitespace
When processing user input or parsing data, you often need to remove leading and trailing whitespace. Java provides several methods for this.
String padded = " Hello World ";
// trim() - removes leading and trailing spaces
String trimmed = padded.trim(); // "Hello World"
// strip() - (Java 11+) Unicode-aware trimming
String stripped = padded.strip(); // "Hello World"
String stripLeft = padded.stripLeading(); // "Hello World "
String stripRight = padded.stripTrailing(); // " Hello World"
// Check if string is blank (Java 11+)
String spaces = " ";
boolean isEmpty = spaces.isEmpty(); // false (has characters)
boolean isBlank = spaces.isBlank(); // true (only whitespace)
Replacing Content
The replace methods create new strings with specified characters or substrings replaced. Remember, the original string remains unchanged.
String text = "Hello World";
// replace(char, char) - replace all occurrences of a character
String replaced = text.replace('o', '0'); // "Hell0 W0rld"
// replace(CharSequence, CharSequence) - replace all substrings
String newText = text.replace("World", "Java"); // "Hello Java"
// replaceAll - uses regex pattern
String digits = "a1b2c3";
String noDigits = digits.replaceAll("[0-9]", ""); // "abc"
String noLetters = digits.replaceAll("[a-z]", ""); // "123"
// replaceFirst - only replaces first occurrence
String repeated = "cat cat cat";
String oneReplaced = repeated.replaceFirst("cat", "dog"); // "dog cat cat"
Splitting Strings
The split() method divides a string into an array of substrings based on a delimiter pattern.
This is essential for parsing CSV data, processing input, and many other tasks.
// Basic split
String csv = "apple,banana,cherry";
String[] fruits = csv.split(",");
// fruits = ["apple", "banana", "cherry"]
// Split by space
String sentence = "Hello World Java";
String[] words = sentence.split(" ");
// words = ["Hello", "World", "Java"]
// Split with limit (max array size)
String data = "a:b:c:d:e";
String[] limited = data.split(":", 3);
// limited = ["a", "b", "c:d:e"]
// Split by multiple characters (regex)
String mixed = "Hello, World; How are you?";
String[] parts = mixed.split("[,;?]\\s*");
// parts = ["Hello", "World", "How are you", ""]
// Be careful with special regex characters!
String path = "C:\\Users\\name\\file.txt";
String[] folders = path.split("\\\\"); // Escape backslash
// folders = ["C:", "Users", "name", "file.txt"]
Joining Strings
String.join() is the opposite of split - it combines multiple strings with a delimiter between them.
// Join array elements
String[] words = {"Hello", "World", "Java"};
String joined = String.join(" ", words); // "Hello World Java"
// Join with different delimiters
String csv = String.join(",", "apple", "banana", "cherry");
// "apple,banana,cherry"
String path = String.join("/", "home", "user", "documents");
// "home/user/documents"
// Join list elements
List<String> items = Arrays.asList("A", "B", "C");
String result = String.join("-", items); // "A-B-C"
String Formatting
String.format() creates formatted strings using format specifiers, similar to printf in C.
// Basic formatting
String name = "Alice";
int age = 25;
String intro = String.format("Name: %s, Age: %d", name, age);
// "Name: Alice, Age: 25"
// Number formatting
double price = 19.99;
String formatted = String.format("Price: $%.2f", price); // "Price: $19.99"
// Padding and alignment
String padded = String.format("%10s", "Hi"); // " Hi" (right-aligned)
String leftPad = String.format("%-10s", "Hi"); // "Hi " (left-aligned)
String zeroPad = String.format("%05d", 42); // "00042"
// Multiple values
String record = String.format("%-10s | %3d | %.2f", "Apple", 5, 1.99);
// "Apple | 5 | 1.99"
String Methods Quick Reference
| Method | Description | Example |
|---|---|---|
length() |
Returns string length | "Hello".length() → 5 |
charAt(i) |
Returns char at index i | "Hello".charAt(1) → 'e' |
substring(s,e) |
Extract from s to e-1 | "Hello".substring(1,4) → "ell" |
indexOf(str) |
First occurrence index | "Hello".indexOf("l") → 2 |
lastIndexOf(str) |
Last occurrence index | "Hello".lastIndexOf("l") → 3 |
contains(str) |
Check if contains | "Hello".contains("ell") → true |
toUpperCase() |
Convert to uppercase | "Hello".toUpperCase() → "HELLO" |
toLowerCase() |
Convert to lowercase | "Hello".toLowerCase() → "hello" |
trim() |
Remove leading/trailing spaces | " Hi ".trim() → "Hi" |
replace(a,b) |
Replace all a with b | "Hello".replace('l','x') → "Hexxo" |
split(regex) |
Split into array | "a,b,c".split(",") → ["a","b","c"] |
toCharArray() |
Convert to char array | "Hi".toCharArray() → ['H','i'] |
Practice Questions: String Methods
Task: Write a method that counts the number of vowels (a, e, i, o, u) in a string.
Show Solution
int countVowels(String str) {
int count = 0;
String vowels = "aeiouAEIOU";
for (char c : str.toCharArray()) {
if (vowels.indexOf(c) != -1) {
count++;
}
}
return count;
}
// Test: countVowels("Hello World") returns 3
Task: Given a file path like "C:/users/docs/file.txt", extract just the file name "file.txt".
Show Solution
String getFileName(String path) {
int lastSlash = path.lastIndexOf('/');
if (lastSlash == -1) {
lastSlash = path.lastIndexOf('\\');
}
return path.substring(lastSlash + 1);
}
// Test: getFileName("C:/users/docs/file.txt") returns "file.txt"
Task: Count how many times a specific word appears in a sentence (case-insensitive).
Show Solution
int countWord(String sentence, String word) {
String[] words = sentence.toLowerCase().split("\\s+");
int count = 0;
for (String w : words) {
if (w.equals(word.toLowerCase())) {
count++;
}
}
return count;
}
// Test: countWord("The cat and the dog", "the") returns 2
Task: Mask all but the last 4 digits of a credit card number with asterisks.
Show Solution
String maskCreditCard(String cardNumber) {
// Remove any spaces or dashes
String clean = cardNumber.replace(" ", "").replace("-", "");
if (clean.length() <= 4) {
return clean;
}
int maskLength = clean.length() - 4;
String lastFour = clean.substring(maskLength);
String masked = "*".repeat(maskLength) + lastFour;
return masked;
}
// Test: maskCreditCard("1234-5678-9012-3456") returns "************3456"
Pattern Matching Algorithms
Pattern matching is a fundamental string operation where we search for occurrences of a pattern within a text.
While indexOf() works for simple cases, understanding efficient algorithms like KMP is essential
for coding interviews and handling large datasets.
Naive Pattern Matching
The simplest approach checks every position in the text as a potential starting point for the pattern. Time complexity is O(n * m) where n is text length and m is pattern length.
// Naive pattern matching - O(n * m)
int naiveSearch(String text, String pattern) {
int n = text.length();
int m = pattern.length();
for (int i = 0; i <= n - m; i++) {
int j;
for (j = 0; j < m; j++) {
if (text.charAt(i + j) != pattern.charAt(j)) {
break;
}
}
if (j == m) {
return i; // Pattern found at index i
}
}
return -1; // Pattern not found
}
// Same as: text.indexOf(pattern)
KMP (Knuth-Morris-Pratt) Algorithm
KMP Pattern Matching
KMP improves upon naive search by using information from previous comparisons. When a mismatch occurs, it uses a precomputed "failure function" (LPS array) to skip unnecessary comparisons. Time complexity is O(n + m).
LPS Array: Longest Proper Prefix which is also Suffix. For pattern "AABAACAABAA", LPS = [0, 1, 0, 1, 2, 0, 1, 2, 3, 4, 5]
// KMP Algorithm - O(n + m)
int[] computeLPS(String pattern) {
int m = pattern.length();
int[] lps = new int[m];
int len = 0;
int i = 1;
while (i < m) {
if (pattern.charAt(i) == pattern.charAt(len)) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
int kmpSearch(String text, String pattern) {
int n = text.length();
int m = pattern.length();
int[] lps = computeLPS(pattern);
int i = 0; // Index for text
int j = 0; // Index for pattern
while (i < n) {
if (pattern.charAt(j) == text.charAt(i)) {
i++;
j++;
}
if (j == m) {
return i - j; // Pattern found
} else if (i < n && pattern.charAt(j) != text.charAt(i)) {
if (j != 0) {
j = lps[j - 1]; // Use LPS to skip comparisons
} else {
i++;
}
}
}
return -1; // Pattern not found
}
// Example usage
String text = "ABABDABACDABABCABAB";
String pattern = "ABABCABAB";
int index = kmpSearch(text, pattern); // Returns 10
indexOf() is sufficient.
| Algorithm | Time Complexity | Space Complexity | Best For |
|---|---|---|---|
| Naive / indexOf() | O(n * m) | O(1) | Short patterns, simple cases |
| KMP | O(n + m) | O(m) | Long texts, repeated searches |
| Rabin-Karp | O(n + m) avg | O(1) | Multiple pattern search |
Interactive: String Method Explorer
Live DemoTry different string methods and see their results instantly.
str.length()
String Comparison
Comparing strings correctly is one of the most common sources of bugs for Java beginners. The difference between
== and .equals() is crucial to understand. This section covers all the ways to compare
strings and when to use each approach.
The == vs .equals() Trap
This is one of the most important concepts in Java string handling. The == operator compares
references (memory addresses), while .equals() compares content (actual characters).
Reference vs Value Comparison
== checks if two variables point to the exact same object in memory.
.equals() checks if two strings have the same sequence of characters,
regardless of where they're stored in memory.
Rule: Always use .equals() for string content comparison. Use ==
only when you specifically need to check if two variables reference the same object.
See how literals vs new keyword affects reference comparison:
// String literals - stored in String Pool
String s1 = "Hello";
String s2 = "Hello";
// Both point to SAME object in String Pool
System.out.println(s1 == s2); // true (same reference)
System.out.println(s1.equals(s2)); // true (same content)
// Using 'new' creates NEW objects in heap
String s3 = new String("Hello");
String s4 = new String("Hello");
// Different objects, DIFFERENT references
System.out.println(s3 == s4); // false (different references!)
System.out.println(s3.equals(s4)); // true (same content)
// Comparing literal with 'new' object
System.out.println(s1 == s3); // false (different references)
System.out.println(s1.equals(s3)); // true (same content)
== to compare strings from user input, file reading, or network
responses will often fail because these create new String objects, not pool references.
Case-Insensitive Comparison
When comparing strings where case doesn't matter (like usernames or commands), use equalsIgnoreCase().
String input = "YES";
String expected = "yes";
// equals() is case-sensitive
System.out.println(input.equals(expected)); // false
// equalsIgnoreCase() ignores case
System.out.println(input.equalsIgnoreCase(expected)); // true
// Alternative: convert both to same case
System.out.println(input.toLowerCase().equals(expected.toLowerCase())); // true
// Practical example: command processing
String command = getUserInput(); // might be "quit", "QUIT", "Quit"
if (command.equalsIgnoreCase("quit")) {
System.out.println("Exiting...");
}
Lexicographic Comparison with compareTo()
The compareTo() method compares strings lexicographically (dictionary order) and returns an integer
indicating the relationship between the strings.
| Return Value | Meaning | Example |
|---|---|---|
< 0 (negative) |
First string comes BEFORE second | "apple".compareTo("banana") → -1 |
0 |
Strings are EQUAL | "hello".compareTo("hello") → 0 |
> 0 (positive) |
First string comes AFTER second | "banana".compareTo("apple") → 1 |
Here's how compareTo() works in practice:
// Basic compareTo usage
String a = "apple";
String b = "banana";
String c = "apple";
System.out.println(a.compareTo(b)); // negative (apple < banana)
System.out.println(b.compareTo(a)); // positive (banana > apple)
System.out.println(a.compareTo(c)); // 0 (equal)
// The actual value is the difference in character codes
System.out.println("a".compareTo("b")); // -1 (97 - 98)
System.out.println("a".compareTo("c")); // -2 (97 - 99)
System.out.println("abc".compareTo("abd")); // -1 (c - d at index 2)
// Case-insensitive version
System.out.println("Apple".compareToIgnoreCase("apple")); // 0
// Useful for sorting
String[] words = {"banana", "apple", "cherry"};
Arrays.sort(words); // Uses compareTo internally
// words = ["apple", "banana", "cherry"]
Handling Null Strings
Calling methods on a null string causes a NullPointerException. Here are safe patterns for comparison.
String userInput = null; // Could be null from form input
// DANGER: This throws NullPointerException!
// if (userInput.equals("test")) { }
// Safe Pattern 1: Check for null first
if (userInput != null && userInput.equals("test")) {
System.out.println("Match!");
}
// Safe Pattern 2: Put literal first (recommended)
if ("test".equals(userInput)) {
System.out.println("Match!"); // Safe even if userInput is null
}
// Safe Pattern 3: Use Objects.equals() (Java 7+)
import java.util.Objects;
if (Objects.equals(userInput, "test")) {
System.out.println("Match!"); // Safe, handles null
}
// Objects.equals handles all null cases
Objects.equals(null, null); // true
Objects.equals(null, "test"); // false
Objects.equals("test", null); // false
Objects.equals("test", "test"); // true
"expected".equals(variable). This prevents NullPointerException if variable is null.
Comparison Methods Summary
| Method | Purpose | Null Safe? |
|---|---|---|
== |
Reference equality (same object) | |
equals() |
Content equality (case-sensitive) | |
equalsIgnoreCase() |
Content equality (case-insensitive) | |
compareTo() |
Lexicographic ordering | |
compareToIgnoreCase() |
Lexicographic ordering (case-insensitive) | |
Objects.equals() |
Content equality (null-safe) |
Practice Questions: String Comparison
Task: What is the output of this code?
String a = "Java";
String b = "Java";
String c = new String("Java");
String d = c.intern();
System.out.println(a == b);
System.out.println(a == c);
System.out.println(a == d);
System.out.println(a.equals(c));
Show Solution
// Output:
// true - a and b reference same pool object
// false - c is a new heap object
// true - d.intern() returns pool reference (same as a)
// true - content comparison is always true for "Java"
Task: Sort an array first by string length (shortest first), then alphabetically for same-length strings.
Show Solution
void customSort(String[] arr) {
Arrays.sort(arr, (s1, s2) -> {
// First compare by length
if (s1.length() != s2.length()) {
return s1.length() - s2.length();
}
// If same length, compare alphabetically
return s1.compareTo(s2);
});
}
// Test: ["cat", "a", "be", "at", "dog"]
// Result: ["a", "at", "be", "cat", "dog"]
Task: Check if two strings are anagrams (contain same characters in different order).
Show Solution
boolean areAnagrams(String s1, String s2) {
// Remove spaces and convert to lowercase
s1 = s1.replaceAll("\\s", "").toLowerCase();
s2 = s2.replaceAll("\\s", "").toLowerCase();
// Different lengths = not anagrams
if (s1.length() != s2.length()) {
return false;
}
// Sort characters and compare
char[] arr1 = s1.toCharArray();
char[] arr2 = s2.toCharArray();
Arrays.sort(arr1);
Arrays.sort(arr2);
return Arrays.equals(arr1, arr2);
}
// Test: areAnagrams("listen", "silent") returns true
StringBuilder - Mutable Strings
When you need to modify strings frequently, especially in loops, StringBuilder is your go-to solution. Unlike regular strings, StringBuilder is mutable, meaning you can change its contents without creating new objects. This makes it significantly more efficient for building strings dynamically.
The Problem with String Concatenation
When you concatenate strings in a loop, each + operation creates a new String object. For large
iterations, this creates thousands of temporary objects, destroying performance.
// BAD: Extremely inefficient for large iterations
String result = "";
for (int i = 0; i < 10000; i++) {
result += i + " "; // Creates 10,000 new String objects!
}
// Time: ~200ms+ for 10,000 iterations
// GOOD: Use StringBuilder for efficiency
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(" "); // Modifies same object
}
String result = sb.toString();
// Time: ~2ms for 10,000 iterations
String vs StringBuilder
String concatenation in loops is O(n2) because each concatenation copies all previous characters. StringBuilder operations are O(n) amortized, making it 100x faster or more for large strings.
Rule of Thumb: Use StringBuilder when concatenating strings in a loop, or when building a string from more than 3-4 parts.
Creating StringBuilder
There are multiple ways to create a StringBuilder. You can start empty, with a specific capacity (useful when you know the approximate final size), or from an existing String.
// Empty StringBuilder (default capacity: 16 characters)
StringBuilder sb1 = new StringBuilder();
// With initial capacity (avoids resizing for known sizes)
StringBuilder sb2 = new StringBuilder(100);
// From existing String
StringBuilder sb3 = new StringBuilder("Hello");
// From CharSequence
StringBuilder sb4 = new StringBuilder(anotherStringBuilder);
// Check capacity and length
sb3.capacity(); // 21 (16 + 5 for "Hello")
sb3.length(); // 5 (actual characters)
Essential StringBuilder Methods
StringBuilder provides methods to modify its content in-place. The most commonly used are
append(), insert(), delete(), and reverse().
All modifying methods return this, enabling method chaining.
StringBuilder sb = new StringBuilder("Hello");
// append() - Add to end (most common method)
sb.append(" World"); // "Hello World"
sb.append(123); // "Hello World123"
sb.append(true); // "Hello World123true"
sb.append('!'); // "Hello World123true!"
// Method chaining - fluent API
sb.append("A").append("B").append("C");
// insert() - Insert at specific position
sb = new StringBuilder("Hello World");
sb.insert(6, "Beautiful "); // "Hello Beautiful World"
sb.insert(0, ">> "); // ">> Hello Beautiful World"
// delete() - Remove characters in range [start, end)
sb = new StringBuilder("Hello World");
sb.delete(5, 11); // "Hello" (removes " World")
// deleteCharAt() - Remove single character
sb = new StringBuilder("Hello");
sb.deleteCharAt(1); // "Hllo"
// replace() - Replace range with new string
sb = new StringBuilder("Hello World");
sb.replace(6, 11, "Java"); // "Hello Java"
// reverse() - Reverse entire content
sb = new StringBuilder("Hello");
sb.reverse(); // "olleH"
// setCharAt() - Change single character
sb = new StringBuilder("Hello");
sb.setCharAt(0, 'J'); // "Jello"
// setLength() - Truncate or extend
sb = new StringBuilder("Hello World");
sb.setLength(5); // "Hello" (truncates)
sb.setLength(10); // "Hello\0\0\0\0\0" (pads with nulls)
Converting to String
After building your string, you need to convert it back to an immutable String object.
The toString() method is the standard way to do this.
StringBuilder sb = new StringBuilder("Hello World");
// toString() - Most common way
String result = sb.toString();
// substring() - Get portion as String
String sub = sb.substring(0, 5); // "Hello"
// Important: StringBuilder is NOT a String
// This won't compile:
// String s = sb; // Error: incompatible types
// This works:
String s = sb.toString();
// For printing, toString() is called automatically
System.out.println(sb); // Prints "Hello World"
StringBuilder vs StringBuffer
| Feature | StringBuilder | StringBuffer |
|---|---|---|
| Thread Safety | Not thread-safe | Thread-safe (synchronized) |
| Performance | Faster (no sync overhead) | Slower (sync overhead) |
| Use Case | Single-threaded applications | Multi-threaded applications |
| Introduced | Java 5 | Java 1.0 |
StringBuilder in 99% of cases. Only use
StringBuffer when multiple threads need to modify the same buffer, which is rare.
Common StringBuilder Patterns
These patterns appear frequently in real-world applications and coding interviews. Master them to efficiently handle string building scenarios without creating unnecessary objects.
Pattern 1: Building Delimited Strings (CSV/TSV)
Joins array elements with a delimiter. The if (i > 0) check ensures no leading delimiter.
Common for CSV generation, query parameters, and log formatting.
String[] values = {"John", "25", "NYC"};
StringBuilder csv = new StringBuilder();
for (int i = 0; i < values.length; i++) {
if (i > 0) csv.append(","); // Add comma before all except first
csv.append(values[i]);
}
// Result: "John,25,NYC"
// Note: Java 8+ has String.join(",", values) for simple cases
Pattern 2: Building Structured Text (HTML/JSON/XML)
Uses method chaining to build multi-line structured content. Ideal for generating markup, configuration files, or formatted output programmatically.
StringBuilder html = new StringBuilder();
html.append("\n");
for (String item : items) {
html.append(" - ").append(item).append("
\n");
}
html.append("
");
// Result for items = ["Apple", "Banana"]:
//
// - Apple
// - Banana
//
Pattern 3: String Reversal (One-Liner)
The most concise way to reverse a string in Java. Creates a StringBuilder, reverses it in-place, and converts back to String. Very common interview question.
String original = "Hello World";
String reversed = new StringBuilder(original).reverse().toString();
// Result: "dlroW olleH"
// This is equivalent to:
StringBuilder sb = new StringBuilder(original);
sb.reverse();
String reversed = sb.toString();
Pattern 4: Palindrome Check
Cleans the string (removes non-alphanumeric, converts to lowercase) then compares with its reverse. Handles edge cases like "A man, a plan, a canal: Panama" correctly.
boolean isPalindrome(String str) {
// Remove non-alphanumeric and convert to lowercase
String cleaned = str.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
// Compare with reverse
return cleaned.equals(
new StringBuilder(cleaned).reverse().toString()
);
}
// "racecar" -> true
// "A man, a plan, a canal: Panama" -> true
// "hello" -> false
Pattern 5: Conditional Join (Skip Nulls/Empty)
Joins elements while filtering out null or empty values. Uses sb.length() > 0 to check
if delimiter is needed. Common for building query strings or optional parameters.
String[] arr = {"Apple", null, "", "Banana", "Cherry"};
StringBuilder sb = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
if (arr[i] != null && !arr[i].isEmpty()) {
if (sb.length() > 0) sb.append(", "); // Add delimiter if not first
sb.append(arr[i]);
}
}
// Result: "Apple, Banana, Cherry" (null and empty skipped)
Practice Questions: StringBuilder
Task: Create a method that repeats a string N times using StringBuilder.
Show Solution
String repeat(String str, int n) {
StringBuilder sb = new StringBuilder(str.length() * n);
for (int i = 0; i < n; i++) {
sb.append(str);
}
return sb.toString();
}
// Test: repeat("abc", 3) returns "abcabcabc"
// Note: Java 11+ has String.repeat(n)
// "abc".repeat(3) returns "abcabcabc"
Task: Remove consecutive duplicate characters from a string.
Show Solution
String removeConsecutiveDuplicates(String str) {
if (str == null || str.length() <= 1) {
return str;
}
StringBuilder sb = new StringBuilder();
sb.append(str.charAt(0));
for (int i = 1; i < str.length(); i++) {
if (str.charAt(i) != str.charAt(i - 1)) {
sb.append(str.charAt(i));
}
}
return sb.toString();
}
// Test: "aabbccaaa" returns "abca"
Task: Implement run-length encoding (e.g., "aaabbc" becomes "a3b2c1").
Show Solution
String compress(String str) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder sb = new StringBuilder();
int count = 1;
for (int i = 1; i <= str.length(); i++) {
if (i < str.length() && str.charAt(i) == str.charAt(i - 1)) {
count++;
} else {
sb.append(str.charAt(i - 1)).append(count);
count = 1;
}
}
// Only return compressed if shorter
return sb.length() < str.length() ? sb.toString() : str;
}
// Test: "aaabbc" returns "a3b2c1"
// Test: "abc" returns "abc" (compressed "a1b1c1" is longer)
Character Operations
Many string problems require working with individual characters. Understanding ASCII values, character conversion, and the Character class methods is essential for solving problems involving character frequency, case conversion, and character classification.
Understanding char and ASCII
In Java, char is a 16-bit unsigned integer representing a Unicode character. For basic English
characters, the ASCII subset (0-127) is most commonly used.
| Character Range | ASCII Values | Description |
|---|---|---|
'0' - '9' |
48 - 57 | Digits |
'A' - 'Z' |
65 - 90 | Uppercase letters |
'a' - 'z' |
97 - 122 | Lowercase letters |
' ' |
32 | Space |
'\n' |
10 | Newline |
Characters can be treated as integers for arithmetic operations. This is powerful for conversion and manipulation:
// char is essentially an integer
char ch = 'A';
System.out.println((int) ch); // 65 (ASCII value)
// Arithmetic with characters
char nextChar = (char) (ch + 1); // 'B'
char lowercase = (char) (ch + 32); // 'a' (A=65, a=97, difference=32)
// Digit to integer conversion
char digit = '7';
int value = digit - '0'; // 7 (not 55!)
// Why? '7' is ASCII 55, '0' is ASCII 48, so 55-48 = 7
// Integer to digit
int num = 5;
char asChar = (char) (num + '0'); // '5'
// Check if character is in range
char c = 'G';
boolean isUppercase = (c >= 'A' && c <= 'Z'); // true
boolean isLetter = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
Character Class Methods
The Character class provides useful static methods for character classification and conversion.
These methods are cleaner and more readable than manual ASCII comparisons.
Classification Methods
These methods return boolean values to check what type of character you're dealing with.
Useful for input validation, parsing, and filtering operations.
char ch = 'A';
// Check character type
Character.isLetter(ch); // true - is it a letter (a-z, A-Z)?
Character.isDigit(ch); // false - is it a digit (0-9)?
Character.isLetterOrDigit(ch); // true - is it alphanumeric?
// Check case
Character.isUpperCase(ch); // true - is it uppercase?
Character.isLowerCase(ch); // false - is it lowercase?
// Check whitespace (space, tab, newline, etc.)
Character.isWhitespace(' '); // true
Character.isWhitespace('\t'); // true (tab)
Character.isWhitespace('\n'); // true (newline)
Conversion Methods
Convert characters between uppercase and lowercase. These methods handle edge cases and non-letter characters safely (returns the original character if not applicable).
// Case conversion
Character.toLowerCase('A'); // 'a'
Character.toUpperCase('a'); // 'A'
Character.toLowerCase('5'); // '5' (unchanged - not a letter)
Character.toUpperCase('@'); // '@' (unchanged - not a letter)
Numeric Value Methods
Extract numeric values from digit characters. Supports hexadecimal digits (A-F represent 10-15). Essential for number parsing and base conversion problems.
// Get numeric value of digit characters
Character.getNumericValue('7'); // 7
Character.getNumericValue('0'); // 0
// Works with hex digits too!
Character.getNumericValue('A'); // 10 (hex digit A)
Character.getNumericValue('F'); // 15 (hex digit F)
Character.getNumericValue('a'); // 10 (case-insensitive)
// Check if valid digit for a specific base (radix)
Character.digit('A', 16); // 10 (valid hex digit)
Character.digit('G', 16); // -1 (invalid - G is not hex)
Character.digit('7', 10); // 7 (valid decimal digit)
Character.digit('7', 8); // 7 (valid octal digit)
Character.digit('9', 8); // -1 (invalid - 9 is not octal)
Converting Between String and char[]
Converting between String and char[] is essential for character-level manipulation.
Remember that toCharArray() creates a copy, so modifying the array doesn't affect the original string.
// String to char array
String str = "Hello";
char[] chars = str.toCharArray();
// chars = ['H', 'e', 'l', 'l', 'o']
// Modify the array
chars[0] = 'J';
// chars = ['J', 'e', 'l', 'l', 'o']
// char array back to String
String newStr = new String(chars); // "Jello"
String newStr2 = String.valueOf(chars); // "Jello"
// Partial conversion
String partial = new String(chars, 1, 3); // "ell" (from index 1, length 3)
// Important: The char[] is a COPY
String original = "Hello";
char[] arr = original.toCharArray();
arr[0] = 'X';
System.out.println(original); // Still "Hello" (unchanged)
Common Character Patterns
These patterns appear frequently in coding interviews and competitive programming. Mastering them will help you solve a wide variety of string and character manipulation problems efficiently.
Pattern 1: Count Character Frequency
Uses an integer array as a simple hash map where the index represents a character and the value represents its count. This is O(n) time and O(1) space for fixed alphabet size.
// Count frequency of lowercase letters
int[] freq = new int[26]; // For lowercase letters
for (char c : str.toCharArray()) {
if (c >= 'a' && c <= 'z') {
freq[c - 'a']++;
}
}
// freq[0] = count of 'a', freq[1] = count of 'b', etc.
// Example: "hello" -> freq[4]=1 (e), freq[7]=1 (h), freq[11]=2 (l), freq[14]=1 (o)
Pattern 2: Check All Unique Characters
Uses a boolean array to track which characters have been seen. Returns false immediately upon finding a duplicate. Common in interview problems like "Is Unique" from Cracking the Coding Interview.
boolean hasAllUnique(String str) {
boolean[] seen = new boolean[128]; // ASCII characters
for (char c : str.toCharArray()) {
if (seen[c]) return false; // Duplicate found
seen[c] = true;
}
return true; // All characters unique
}
// "abcdef" -> true, "hello" -> false (duplicate 'l')
Pattern 3: Manual Case Conversion
Exploits the 32-character gap between uppercase (65-90) and lowercase (97-122) ASCII values.
Faster than Character.toUpperCase() for performance-critical code.
// ASCII: 'A'=65, 'a'=97, difference = 32
char toUpper = (char) (ch - 32); // Only if ch is lowercase
char toLower = (char) (ch + 32); // Only if ch is uppercase
// Safe version with check
char safeToUpper(char c) {
return (c >= 'a' && c <= 'z') ? (char)(c - 32) : c;
}
Pattern 4: Check if Vowel
Simple helper function used in many string problems involving vowel counting, removal, or manipulation. Converting to lowercase first handles both cases with a single comparison set.
boolean isVowel(char c) {
c = Character.toLowerCase(c);
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
}
// Alternative using indexOf (more concise)
boolean isVowel(char c) {
return "aeiouAEIOU".indexOf(c) != -1;
}
Pattern 5: Character Iteration
Two common approaches: index-based with charAt() (no extra memory) or enhanced for-loop with
toCharArray() (creates copy but cleaner syntax). Choose based on whether you need the index.
// Method 1: Index-based (no extra memory, access to index)
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// Can use 'i' for position-based logic
}
// Method 2: Enhanced for-loop (creates copy, cleaner syntax)
for (char c : str.toCharArray()) {
// Use when index not needed
}
Unicode Considerations
char supports the Basic Multilingual Plane (BMP) of Unicode.
For characters outside BMP (like some emojis), you need to work with code points.
While ASCII (0-127) covers basic English characters, real-world applications often deal with international text, special symbols, and emojis. Java uses Unicode internally, and understanding how to work with it properly ensures your code handles diverse character sets correctly.
Unicode Escape Sequences
You can represent any Unicode character using escape sequences in the format \uXXXX,
where XXXX is the 4-digit hexadecimal code point. This is useful when you need to include special
characters that aren't on your keyboard or when working with internationalized text.
// Unicode escape sequences
char omega = '\u03A9'; // Greek uppercase omega (Ω)
String heart = "\u2665"; // Heart symbol (♥)
char pi = '\u03C0'; // Greek lowercase pi (π)
String smiley = "\u263A"; // Smiley face (☺)
System.out.println("Greek letter omega: " + omega); // Ω
System.out.println("Heart symbol: " + heart); // ♥
System.out.println("Value of " + pi + " is ~3.14"); // Value of π is ~3.14
Working with Code Points
Every character in Unicode has a unique numeric value called a code point. While
char can hold characters in the Basic Multilingual Plane (BMP, code points 0-65535),
characters outside this range (like many emojis) require special handling using code point methods.
// Getting Unicode code point of a character
String text = "Hello";
int codePoint = text.codePointAt(0);
System.out.println(codePoint); // 72 (ASCII/Unicode for 'H')
// Code points for special characters
String omega = "Ω";
System.out.println(omega.codePointAt(0)); // 937 (0x03A9 in hex)
// Convert code point back to character
int cp = 937;
String fromCodePoint = Character.toString(cp);
System.out.println(fromCodePoint); // Ω
Safe Iteration Through All Code Points
When dealing with text that might contain characters outside the BMP (like emojis or rare Chinese
characters), iterating with charAt() can break because these characters use two char
values (surrogate pairs). Use codePoints() stream for safe iteration:
// Safe way to iterate through all code points
String text = "Hello";
text.codePoints().forEach(cp -> {
System.out.println(Character.toString(cp));
});
// Output: H, e, l, l, o (each on separate line)
// Count actual characters (not char units)
String withEmoji = "Hi!"; // Assuming emoji at end
int charCount = withEmoji.length(); // May be incorrect for emojis
int codePointCount = (int) withEmoji.codePoints().count(); // Correct count
// Process each code point with index
int[] codePoints = text.codePoints().toArray();
for (int i = 0; i < codePoints.length; i++) {
System.out.println("Position " + i + ": " + Character.toString(codePoints[i]));
}
codePoints() instead of charAt() for safe character handling.
Practice Questions: Character Operations
Task: Count the number of vowels and consonants in a string.
Show Solution
int[] countVowelsConsonants(String str) {
int vowels = 0, consonants = 0;
str = str.toLowerCase();
for (char c : str.toCharArray()) {
if (Character.isLetter(c)) {
if ("aeiou".indexOf(c) != -1) {
vowels++;
} else {
consonants++;
}
}
}
return new int[]{vowels, consonants};
}
// Test: "Hello World" returns [3, 7]
Task: Find the first character that appears only once in the string.
Show Solution
char firstNonRepeating(String str) {
int[] freq = new int[256]; // Extended ASCII
// Count frequencies
for (char c : str.toCharArray()) {
freq[c]++;
}
// Find first with frequency 1
for (char c : str.toCharArray()) {
if (freq[c] == 1) {
return c;
}
}
return '\0'; // No non-repeating character
}
// Test: "leetcode" returns 'l'
// Test: "aabb" returns '\0'
Task: Two strings are isomorphic if characters in one can be mapped to characters in another consistently (e.g., "egg" and "add").
Show Solution
boolean isIsomorphic(String s, String t) {
if (s.length() != t.length()) return false;
int[] mapS = new int[256]; // s char -> t char mapping
int[] mapT = new int[256]; // t char -> s char mapping
for (int i = 0; i < s.length(); i++) {
char cs = s.charAt(i);
char ct = t.charAt(i);
// Check if mapping exists and matches
if (mapS[cs] != mapT[ct]) {
return false;
}
// Record the mapping (use i+1 to distinguish from default 0)
mapS[cs] = i + 1;
mapT[ct] = i + 1;
}
return true;
}
// Test: isIsomorphic("egg", "add") returns true
// Test: isIsomorphic("foo", "bar") returns false
Key Takeaways
Strings are Immutable
Once created, a String object cannot be changed - any modification creates a new String object
Use .equals() for Comparison
Never use == for string content comparison - it compares references, not values
StringBuilder for Loops
Use StringBuilder when building strings in loops to avoid O(n^2) time complexity
String Pool Optimization
String literals are stored in a pool for memory efficiency - use literals when possible
Character Methods Matter
The Character class provides essential methods for case checking, digit validation, and conversion
Know Your Complexity
String concatenation is O(n), substring is O(n), and charAt is O(1) - choose wisely
Knowledge Check
Test your understanding of Java String operations:
What does "String immutability" mean in Java?
What is the output of: "Hello".substring(1, 4)?
Which comparison will return true?
String s1 = new String("hello");
String s2 = new String("hello");
Why is StringBuilder preferred over String concatenation in loops?
What does Character.isLetterOrDigit('5') return?
What is the ASCII value difference between 'a' and 'A'?