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
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.
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
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.
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
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());
}
}
length() returns the number of characters.
charAt(0) returns the first character.
toLowerCase() returns a new string with all letters lowercased.
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);
}
}
String literals with the same text are interned, so a and b point to the same object.
new String(...) always creates a separate object, so a == c is false.
Concatenation creates a new string; the original a is unchanged.
What does s = s + '!'; do when s is "Hi"?
Strings are immutable. The + operator builds a new String object containing "Hi!" and assigns that new reference to s. The original "Hi" object is unchanged.
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);
}
}
concat returns a new string; it does not modify greeting.
toUpperCase also returns a new string; shout points to it.
greeting itself never changes.
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());
}
}
append adds text to the existing buffer.
reverse flips the buffer in place.
toString() produces a String from the current buffer contents.
Recap
Stringis 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
StringBuilderfor 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.