Module 2 · Core Syntax & Types ⏱ 19 min

Strings & Text

By the end of this lesson you will be able to:
  • Explain why String is immutable and what that means for reassignment
  • Predict the result of == versus .equals() on strings
  • Use string concatenation and common methods correctly
  • Choose StringBuilder when mutable text construction is needed

A String in Java is an object, not a primitive — and it is immutable. Once created, its characters never change. Operations like + or .replace() do not edit the string in place; they build a brand-new String and return it. That design makes strings safe to share across threads and methods, but it also means reassignment and mutation are two different things. Understanding that distinction prevents some of the most common Java bugs.

Immutability in practice

When you write String s = "Hi"; s = s + "!";, the original "Hi" object is not modified. Instead, + creates a new "Hi!" object and stores its address in s. The old "Hi" still exists in memory until the garbage collector reclaims it.

This immutability is deliberate. It means strings can be shared safely: if ten variables all point to "Config", none of them can accidentally change it for the others. Because strings are immutable, they are also inherently thread-safe: multiple threads can read the same string without locks or synchronization. The trade-off is performance: heavy string concatenation in a loop creates many temporary objects, which is why StringBuilder exists.

flowchart TD
  A["String literal"] --> P["String pool"]
  B["new String(...)"] --> H["Heap"]
  P --> R1["a == b → true"]
  H --> R2["a == c → false"]
  P --> R3["a.equals(c) → true"]
  H --> R3
  style P fill:#c2410c,color:#fff
  style H fill:#fed7aa
String literals live in the string pool; new String creates a separate heap object.
Main.java — immutability means reassignment creates a new object. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        String a = "Hi";
        String b = a;
        a = a + "!";
        System.out.println(a);
        System.out.println(b);
    }
}

== versus .equals()

== asks whether two references point at the exact same object in memory. .equals() asks whether two strings contain the same characters. Two string literals with the same text may share one object (the string pool), but new String(...) always creates a separate object — so == can be false even when the text is identical.

The string pool is an optimisation: literals like "Hi" are reused to save memory. But you cannot rely on it for logic. Always use .equals() when you care about the text. The .equals() method is defined in the Object class and overridden by String to compare character by character. Forgetting to override .equals() in your own classes is a common advanced bug, but for now, just remember that strings have it done correctly.

Main.java — == checks identity; .equals() checks content. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        String a = "Test";
        String b = "Test";
        String c = new String("Test");
        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a.equals(c));
    }
}
flowchart LR
  subgraph Ref["== checks identity"]
    A1["a → Hi"]
    B1["b → Hi"]
    C1["c → Hi (new)"]
    A1 -.->|same object| B1
    A1 -.->|different object| C1
  end
  subgraph Eq[".equals() checks content"]
    A2["a → Hi"]
    C2["c → Hi (new)"]
    A2 -->|same text| C2
  end
  style Ref fill:#b45309,color:#fff
  style Eq fill:#166534,color:#fff
== compares memory addresses; .equals() compares the actual characters.

Common string methods

Java's String class provides a rich library of methods. .length() returns the number of characters. .charAt(index) returns the character at a position, starting from zero. .substring(start, end) extracts a slice. .indexOf(text) finds where a substring first appears, or -1 if it is absent. .toUpperCase() and .toLowerCase() return new strings with changed case. .trim() removes leading and trailing whitespace.

All of these return new strings; none modify the original. .isEmpty() returns true when length is zero, and .isBlank() (Java 11+) returns true when the string contains only whitespace. These small conveniences make validation code far more readable than checking lengths manually. If you need to know whether a string contains a word, use .contains("word") rather than manually searching with .indexOf.

Main.java — common string methods. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        String name = "  Ada Lovelace  ";
        System.out.println(name.trim());
        System.out.println(name.toUpperCase());
        System.out.println(name.contains("Ada"));
        System.out.println(name.length());
    }
}

Escape sequences and special characters

Strings can contain characters you cannot type directly, using escape sequences. A backslash followed by n inserts a newline, t inserts a tab, and a double backslash inserts a literal backslash. A backslash followed by a quote inserts a literal quote without ending the string. These are processed when the compiler reads your source, so a string with an embedded newline escape prints on two lines. Forgetting to escape a backslash — especially in file paths on Windows — is a common source of compilation errors.

StringBuilder: when you need mutability

If you are building a string in a loop — concatenating lines from a file, assembling a report, or formatting a message — using + repeatedly is slow because it creates a new String object on every iteration. StringBuilder solves this by maintaining a mutable buffer. You call .append(), .insert(), or .replace() on the same object, and when you are done you call .toString() to get the final text.

Unlike string concatenation, StringBuilder does not create intermediate objects, so appending a thousand items is roughly a thousand times faster than using + in a loop. StringBuilder is not thread-safe; if multiple threads modify it at once, use StringBuffer instead. For single-threaded code, StringBuilder is the standard choice.

flowchart LR
  SB["Hi"] --> A1["append !"]
  A1 --> SB2["Hi!"]
  SB2 --> A2["append ?"]
  A2 --> SB3["Hi!?"]
  style SB fill:#c2410c,color:#fff
  style SB3 fill:#166534,color:#fff
StringBuilder mutates the same buffer; String concatenation creates new objects.
Exercise

What does this program print? Read it carefully and type the exact output (three lines).

public class Main {
    public static void main(String[] args) {
        String name = "Ada";
        System.out.println(name.length());
        System.out.println(name.charAt(0));
        System.out.println(name.toLowerCase());
    }
}
Exercise

What does this program print? Read it carefully and type the exact output (five lines).

public class Main {
    public static void main(String[] args) {
        String a = "Hi";
        String b = "Hi";
        String c = new String("Hi");
        String d = a + "!";

        System.out.println(a == b);
        System.out.println(a == c);
        System.out.println(a.equals(c));
        System.out.println(d);
        System.out.println(a);
    }
}
Exercise

What does s = s + '!'; do when s is "Hi"?

Exercise

This code tries to modify a string and uppercase it, but one line does nothing. Predict the exact three-line output.

public class Main {
    public static void main(String[] args) {
        String greeting = "Hello";
        greeting.concat(" World");
        System.out.println(greeting);
        String shout = greeting.toUpperCase();
        System.out.println(shout);
        System.out.println(greeting);
    }
}
Exercise

StringBuilder mutates in place. Predict the exact two-line output.

public class Main {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Java");
        sb.append(" is");
        sb.append(" fun");
        System.out.println(sb.toString());
        System.out.println(sb.reverse().toString());
    }
}

Recap

  • String is immutable: +, .replace(), and .toUpperCase() all return new strings.
  • == checks object identity; .equals() checks character content. Use .equals() for text.
  • String literals may be pooled, but new String(...) always creates a separate heap object.
  • Common methods: .length(), .charAt(), .substring(), .indexOf(), .contains(), .trim().
  • Use StringBuilder for mutable string construction, especially inside loops.

Next you will learn about Java's control flow — how to make decisions and repeat actions with conditionals and loops.

Checkpoint quiz

Which expression correctly checks whether two strings have the same characters?

What is printed by String x = "A"; x = x + "B"; System.out.println(x);?

Go deeper — technical resources