Skip to content

06 · Strings & String Formatting

🎥 Video walkthrough

String basics

String is a reference type, but Java gives it special syntax support (literals, + concatenation). Strings are immutable — every "modifying" method actually returns a new String.

String first = "Ada";
String last = "Lovelace";
String full = first + " " + last;

System.out.println(full);            // Ada Lovelace
System.out.println(full.length());    // 12
System.out.println(full.toUpperCase()); // ADA LOVELACE
System.out.println(full.toLowerCase()); // ada lovelace
System.out.println(full.contains("Love")); // true
System.out.println(full.indexOf("Lovelace")); // 4
System.out.println(full.replace("Ada", "Grace")); // Grace Lovelace
System.out.println(full.substring(4));   // Lovelace
System.out.println(full.substring(0, 3)); // Ada

Because strings are immutable, full.toUpperCase() does not change full — it returns a brand-new String. You must capture the result if you want to keep it:

String name = "ada";
name = name.toUpperCase();   // reassign to keep the change
System.out.println(name);    // ADA

Comparing strings

Never use == to compare string contents — it compares object references, not text. Use .equals():

String a = new String("hi");
String b = new String("hi");

System.out.println(a == b);        // false -- different objects!
System.out.println(a.equals(b));   // true  -- same content

System.out.println(a.equalsIgnoreCase("HI"));  // true

Splitting and joining

String csv = "apple,banana,cherry";
String[] fruits = csv.split(",");
System.out.println(fruits[1]);   // banana

String rejoined = String.join(" | ", fruits);
System.out.println(rejoined);    // apple | banana | cherry

String padded = "  hello  ";
System.out.println(padded.trim());   // "hello" -- trims whitespace

StringBuilder — efficient concatenation

Every + on strings creates a new object; in a loop that's wasteful. StringBuilder mutates an internal buffer instead:

StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
    sb.append("Item ").append(i).append("\n");
}
String result = sb.toString();
System.out.print(result);
// Item 1
// Item 2
// Item 3
// Item 4
// Item 5

sb.insert(0, "=== List ===\n");
sb.reverse();  // also supports reverse, delete, replace, etc.

Formatting output

String.format and printf use format specifiers similar to C's printf:

String name = "Ada";
int age = 36;
double gpa = 3.95;

String formatted = String.format("%s is %d years old, GPA %.2f", name, age, gpa);
System.out.println(formatted);
// Ada is 36 years old, GPA 3.95

System.out.printf("%-10s|%5d%n", "Alice", 42);   // left-pad name, right-pad number
System.out.printf("%-10s|%5d%n", "Bob", 7);
// Alice     |   42
// Bob       |    7
Specifier Meaning
%s String
%d Integer
%f Floating-point (%.2f = 2 decimal places)
%n Platform-independent newline
%-10s Left-justify in a 10-character field
%5d Right-justify integer in a 5-character field

Text blocks (Java 15+)

For multi-line strings, use triple-quoted text blocks instead of \n concatenation:

String html = """
        <html>
          <body>Hello</body>
        </html>
        """;
System.out.print(html);

How It Actually Works

String objects are backed by an immutable byte[] (since Java 9's Compact Strings: Latin-1 content is stored 1 byte/char, and only strings containing real UTF-16 characters fall back to 2 bytes/char, tracked by a coder flag) — immutability is enforced by the final field and no mutator methods, which is what makes strings safe to share across threads without synchronization and safe to use as HashMap keys (the hash is computed once and cached in a hash field).

String literals are interned: the compiler places them in a JVM-managed string pool (part of the heap since Java 7), and two identical literals resolve to the very same object via ldc's constant-pool lookup — which is why "abc" == "abc" is true but new String("abc") == "abc" is false; new String(...) forces a fresh heap allocation that bypasses the pool.

String concatenation with + in a loop is a trap for a mechanical reason: each + in older bytecode compiled to a fresh StringBuilder append-and-toString() round trip; modern javac (post-JEP 280) instead emits an invokedynamic call to StringConcatFactory, which builds one optimized concatenation recipe at the call site — but inside a loop it still allocates and discards immutable string garbage every iteration, which is exactly why a single StringBuilder reused across iterations avoids that GC churn.

🔀 See this in another language

Exercise

Write a program that builds a simple receipt: given parallel arrays of item names (String[]) and prices (double[]), use a StringBuilder to build a multi-line receipt where each line is formatted with String.format so item names are left-aligned in a 15-character field and prices are right-aligned with two decimal places, followed by a final "Total" line.