Skip to main content
Java beginner Lesson 9 of 58

Strings in Java

Deep dive into Java String — immutability, common methods, StringBuilder, StringBuffer, string formatting, and regular expressions.

String is one of the most-used classes in Java. Understanding how it works — especially immutability and the string pool — prevents subtle bugs and performance issues that are otherwise very hard to diagnose.

String Basics

There are two ways to create a String. String literals are stored in the string pool — Java reuses the same object for identical literals, saving memory. Using new String(...) bypasses the pool and always creates a fresh object, which is almost never what you want.

String s1 = "Hello";              // string literal — stored in pool
String s2 = new String("Hello");  // new object — bypasses pool
String empty = "";                 // empty string, length 0
String blank = "   ";              // not empty, but blank

Immutability

Every string operation that looks like a modification creates a new String — the original is left unchanged. This is not a flaw: it is what makes strings safe to pass to methods, use as map keys, and share across threads. The practical consequence is that you must reassign the variable to capture the result.

String s = "hello";
s.toUpperCase();         // returns a new String — does NOT modify s
System.out.println(s);  // still "hello"

s = s.toUpperCase();     // reassign to capture the new String
System.out.println(s);  // "HELLO"

== vs .equals()

== compares object identity (are these the same object in memory?), not content. For strings, always use .equals() to compare content. Relying on == produces correct results with pooled literals but fails silently with new String(...) or strings built at runtime.

String a = "java";
String b = "java";
String c = new String("java");

System.out.println(a == b);        // true  — same pool object
System.out.println(a == c);        // false — c is a different object
System.out.println(a.equals(c));   // true  — same content (always use this)
System.out.println(a.equalsIgnoreCase("JAVA")); // true

Always use .equals() to compare string content.

Common String Methods

These are the methods you will reach for most often. Every method returns a new String — remember to capture the result if you need it.

String s = "  Hello, Java World!  ";

// Length and characters
s.length()              // 22
s.charAt(7)             // 'J'
s.indexOf("Java")       // 8
s.lastIndexOf("o")      // 16
s.isEmpty()             // false
s.isBlank()             // false (Java 11+)

// Case and whitespace
s.trim()                // "Hello, Java World!"   (removes ASCII whitespace)
s.strip()               // "Hello, Java World!"   (Unicode-aware, Java 11+)
s.toLowerCase()         // "  hello, java world!  "
s.toUpperCase()         // "  HELLO, JAVA WORLD!  "

// Testing content
s.contains("Java")      // true
s.startsWith("  Hello") // true
s.endsWith("!  ")       // true

// Extracting substrings
s.substring(8, 12)      // "Java"
s.substring(8)          // "Java World!  "

// Replacing
s.replace("Java", "Python")          // "  Hello, Python World!  "
s.replaceAll("\\s+", "-")            // "--Hello,-Java-World!--"
s.replaceFirst("[A-Z]", "X")         // "  Xello, Java World!  "

// Splitting
"a,b,c".split(",")      // ["a", "b", "c"]
"one two three".split("\\s+") // ["one", "two", "three"]

// Joining (Java 8+)
String.join(", ", "Alice", "Bob", "Charlie")  // "Alice, Bob, Charlie"
String.join("-", List.of("2024", "01", "15")) // "2024-01-15"

String Comparison Methods

compareTo is used for sorting — it returns a negative number, zero, or positive number indicating the lexicographic order. Collections and sorted data structures use this internally when you sort strings.

"apple".compareTo("banana")    // negative (a < b)
"banana".compareTo("apple")    // positive
"apple".compareTo("apple")     // 0 (equal)
"Apple".compareToIgnoreCase("apple") // 0

String Formatting

String formatting lets you build readable output without messy concatenation chains. The format specifiers match the type of the value being inserted and control width, precision, and alignment.

// String.format — like printf but returns a String
String msg = String.format("Name: %s, Age: %d, Score: %.2f", "Alice", 25, 98.5);
// "Name: Alice, Age: 25, Score: 98.50"

// Common format specifiers
// %s  — String
// %d  — integer (int, long)
// %f  — floating-point (%,.2f for comma separator + 2 decimal places)
// %n  — newline (platform-safe)
// %b  — boolean
// %c  — char
// %x  — hexadecimal

// printf — formats and prints directly
System.out.printf("%-15s %5d%n", "Alice", 95); // left-align name, right-align score

// Text blocks (Java 15+) — multi-line strings with clean indentation
String json = """
        {
            "name": "Alice",
            "age": 25,
            "active": true
        }
        """;

StringBuilder — Efficient String Building

String concatenation in a loop creates a new object on every iteration — for 1000 iterations that is 1000 throwaway String objects that the garbage collector then has to clean up. StringBuilder solves this by maintaining a single mutable character buffer and producing one String only at the end.

// Bad — creates 1000 String objects
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;  // new String each time
}

// Good — single mutable buffer, far less garbage
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();  // one final String

StringBuilder Methods

StringBuilder sb = new StringBuilder("Hello");

sb.append(", World");      // "Hello, World"
sb.insert(5, " there");    // "Hello there, World"
sb.delete(5, 11);          // "Hello, World"
sb.replace(7, 12, "Java"); // "Hello, Java"
sb.reverse();              // "avaJ ,olleH"
sb.reverse();              // "Hello, Java"  (reversed back)

sb.length()                // 11
sb.charAt(0)               // 'H'
sb.indexOf("Java")         // 7
sb.toString()              // "Hello, Java"

// Method chaining — each mutating method returns the same StringBuilder
String csv = new StringBuilder()
    .append("Alice")
    .append(",")
    .append(30)
    .append(",")
    .append("Engineer")
    .toString();  // "Alice,30,Engineer"

StringBuffer

StringBuffer has the same API as StringBuilder but all methods are synchronized — meaning it is safe for multiple threads to use the same instance concurrently. In practice, string building is almost always local to a single method, so prefer StringBuilder unless you have a concrete threading requirement.

StringBuffer buf = new StringBuffer();
buf.append("thread-safe");
buf.insert(0, "This is ");
System.out.println(buf.toString()); // "This is thread-safe"

Regular Expressions

Regular expressions let you describe text patterns rather than literal strings. They are the right tool for validation (does this look like an email?), extraction (pull all phone numbers from a document), and complex replacement. Java uses the java.util.regex package, and many String methods accept regex patterns directly.

import java.util.regex.*;

String email = "alice@example.com";

// matches() — entire string must match the pattern
boolean valid = email.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
System.out.println(valid); // true

// String methods that accept regex
"hello world".replaceAll("[aeiou]", "*") // "h*ll* w*rld"
"one1two2three3".split("\\d")            // ["one", "two", "three"]
"2024-01-15".matches("\\d{4}-\\d{2}-\\d{2}") // true

Common Regex Patterns

PatternMatches
\\dAny digit (0–9)
\\wWord character (letter, digit, _)
\\sWhitespace
.Any character
[abc]a, b, or c
[^abc]Not a, b, or c
[a-z]Lowercase letter
a+One or more a
a*Zero or more a
a?Zero or one a
a{3}Exactly three a
^Start of string
$End of string

Pattern and Matcher — for Repeated Use

Compiling a regex into a Pattern once and reusing it is significantly faster than calling String.matches() in a loop, because matches() recompiles the pattern every call.

Pattern phonePattern = Pattern.compile("\\(?(\\d{3})\\)?[-.\\s]?(\\d{3})[-.\\s]?(\\d{4})");

String[] inputs = {"(555) 123-4567", "555.123.4567", "invalid"};
for (String input : inputs) {
    Matcher m = phonePattern.matcher(input);
    if (m.matches()) {
        System.out.printf("Area: %s, Number: %s-%s%n", m.group(1), m.group(2), m.group(3));
    } else {
        System.out.println("Not a valid phone: " + input);
    }
}

Projects

Password Validator

public class PasswordValidator {

    public static boolean isValid(String password) {
        if (password.length() < 8) return false;
        if (!password.matches(".*[A-Z].*")) return false;  // at least one uppercase
        if (!password.matches(".*[a-z].*")) return false;  // at least one lowercase
        if (!password.matches(".*\\d.*"))   return false;  // at least one digit
        if (!password.matches(".*[!@#$%^&*].*")) return false; // at least one special char
        return true;
    }

    public static String feedback(String password) {
        StringBuilder issues = new StringBuilder();
        if (password.length() < 8)                       issues.append("At least 8 characters. ");
        if (!password.matches(".*[A-Z].*"))              issues.append("Add an uppercase letter. ");
        if (!password.matches(".*[a-z].*"))              issues.append("Add a lowercase letter. ");
        if (!password.matches(".*\\d.*"))                issues.append("Add a digit. ");
        if (!password.matches(".*[!@#$%^&*].*"))         issues.append("Add a special character (!@#$%^&*). ");
        return issues.length() == 0 ? "Strong password!" : issues.toString().trim();
    }

    public static void main(String[] args) {
        String[] passwords = {"pass", "Password1", "P@ssword1"};
        for (String p : passwords) {
            System.out.printf("%-15s → %s%n", p, feedback(p));
        }
    }
}

Text Analyzer

public class TextAnalyzer {
    public static void main(String[] args) {
        String text = "The quick brown fox jumps over the lazy dog. " +
                      "Pack my box with five dozen liquor jugs.";

        String[] words = text.toLowerCase().split("[^a-z]+");
        int wordCount   = words.length;
        int charCount   = text.replaceAll("\\s", "").length();
        int sentenceCount = text.split("[.!?]+").length;

        System.out.println("Words:     " + wordCount);
        System.out.println("Chars:     " + charCount);
        System.out.println("Sentences: " + sentenceCount);

        // Count vowels using the chars() stream
        long vowels = text.chars()
            .filter(c -> "aeiouAEIOU".indexOf(c) >= 0)
            .count();
        System.out.println("Vowels:    " + vowels);

        // Most frequent word using a frequency map
        java.util.Map<String, Integer> freq = new java.util.HashMap<>();
        for (String w : words) {
            if (!w.isEmpty()) freq.merge(w, 1, Integer::sum);
        }
        String topWord = freq.entrySet().stream()
            .max(java.util.Map.Entry.comparingByValue())
            .map(java.util.Map.Entry::getKey)
            .orElse("");
        System.out.println("Top word:  \"" + topWord + "\" (" + freq.get(topWord) + "x)");
    }
}

Frequently Asked Questions

Why is String immutable in Java?
Immutability enables the string pool (interning), makes Strings safe to share across threads, and allows them to be used as HashMap keys reliably. Every 'modification' creates a new String object — the original is unchanged.
When should I use StringBuilder vs StringBuffer?
StringBuilder is faster and should be your default — it is not thread-safe, but that's fine for local variables. Use StringBuffer only when multiple threads share the same builder, which is rare.
What is the string pool?
String literals are stored in a special area of the heap called the string pool and are reused. String a = "hello"; String b = "hello"; — both a and b point to the same object. Using new String("hello") bypasses the pool and creates a separate object.