Module 2.2

String Operations and Manipulation

Master the essential string operations in Java. Understand immutability, learn StringBuilder for efficient manipulation, explore character operations, and practice algorithms that appear frequently in coding interviews.

40 min read
Beginner
Hands-on Examples
What You'll Learn
  • String immutability and the String pool
  • Essential String methods for manipulation
  • String comparison techniques
  • StringBuilder for efficient string building
  • Character operations and ASCII manipulation
Contents
01

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.

Core Concept

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!
Best Practice: Always use string literals when possible. They are stored in the String Pool and are more memory-efficient than using the 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
Memory Tip: In a loop that concatenates strings, each iteration creates a new String object. For 1000 iterations, that's 1000 objects! This is why StringBuilder exists.

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
}
Common Error: Accessing an index outside the valid range (0 to length-1) throws 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)
02

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"
Index Rule: 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

Efficient 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
When to Use KMP: Use KMP when searching for patterns in very long texts, or when you need to find all occurrences of a pattern. For simple single searches, 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 Demo

Try different string methods and see their results instantly.

Java Code: str.length()
Result: 11
03

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).

Critical Difference

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)
Common Bug: Using == 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
Pro Tip: When comparing a variable with a literal, put the literal first: "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
04

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
Performance

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
When to Choose: Use 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)
05

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

Beyond ASCII: Java's 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]));
}
DSA Interview Tip: For most DSA problems, you only need ASCII. However, be aware of edge cases when the problem mentions "international text" or "any Unicode characters." In such cases, use 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:

Question 1 of 6

What does "String immutability" mean in Java?

Question 2 of 6

What is the output of: "Hello".substring(1, 4)?

Question 3 of 6

Which comparison will return true?

String s1 = new String("hello");
String s2 = new String("hello");
Question 4 of 6

Why is StringBuilder preferred over String concatenation in loops?

Question 5 of 6

What does Character.isLetterOrDigit('5') return?

Question 6 of 6

What is the ASCII value difference between 'a' and 'A'?

Answer all questions to check your score