Java Immutable Strings

In the last lesson you learned about Java StringBuffer. Along the way you kept hearing one word: a String is immutable. What does that actually mean, and why did Java’s makers choose it?

🤔 What does immutable even mean?

The pain: you call toUpperCase() on a lowercase name, print it, and nothing changed.

  • Immutable means once a String is made, its contents can never change.
  • A method like toUpperCase() cannot edit the String, so it builds a brand new one and returns it.
  • Your original stays untouched.
  • If you do not catch the new String, it is lost.

This is the whole idea in one short program.

public class Demo {
public static void main(String[] args) {
String name = "alex";
name.toUpperCase(); // makes "ALEX" then throws it away
System.out.println(name); // still lowercase
}
}

Walking through it:

  • toUpperCase() builds a new String “ALEX”.
  • We never store it, so it is created and immediately lost.
  • name still points at the same old “alex”. That is immutability in action.

Output

alex

🔁 Methods return a new String

If methods cannot change a String, what do they do? They return a new one.

  • Every “modifying” method works this way: concat, toUpperCase, replace, trim, substring.
  • Each builds a fresh String from the old one and gives it back.
  • The original stays exactly as it was.

This proves it with three methods.

public class NewStrings {
public static void main(String[] args) {
String s = "Hello";
String upper = s.toUpperCase(); // new String "HELLO"
String joined = s.concat(" World"); // new String "Hello World"
String swapped = s.replace('l', 'L'); // new String "HeLLo"
System.out.println("upper: " + upper);
System.out.println("joined: " + joined);
System.out.println("swapped: " + swapped);
System.out.println("original s: " + s); // unchanged
}
}

Line by line:

  • toUpperCase() returns “HELLO” into upper.
  • concat(" World") returns “Hello World” into joined.
  • replace('l', 'L') returns “HeLLo” into swapped.
  • Three new Strings were born, but s is still “Hello”. It never changed, because it cannot.

Output

upper: HELLO
joined: Hello World
swapped: HeLLo
original s: Hello

The takeaway: a String method gives you a result, it does not update what you called it on. Always look at the return value.

💾 You must reassign to keep the result

You do not “change” a String. You point your variable at the new String the method returns. That is called reassigning.

Here is the fix for that first program.

public class Reassign {
public static void main(String[] args) {
String name = "alex";
name = name.toUpperCase(); // ✅ catch the new String, point name at it
System.out.println(name); // now uppercase
}
}

The line name = name.toUpperCase():

  • First the method builds the new String “ALEX”.
  • Then the = points name at it. The old “alex” is forgotten.
  • The variable moved. The String itself still never changed.

Output

ALEX

You can chain methods too. This trims, lowercases, then replaces, all stored back into one variable.

String input = " Hello World ";
input = input.trim().toLowerCase().replace(' ', '-');
System.out.println(input); // hello-world

How the chain works:

  • Each method returns a new String, and the next method runs on that.
  • At the end, the = points input at the final result.
  • Without that =, every result would be thrown away.

Output

hello-world

🔐 Why is String immutable?

It feels like extra work, but it was a deliberate choice that buys a lot of safety.

  • Safe sharing in the string pool. Java stores text literals in the string pool and hands the same String object to parts that use the same text. This is safe only because no one can change it.
  • Thread safety. A String can never change, so two threads can never corrupt it. Many threads read it freely with no locks.
  • Safe as HashMap keys. A HashMap finds a key by its hashcode. A changeable key could change its hashcode and get lost. A String key stays findable forever.
  • Cached hashcode. Since a String never changes, Java computes its hashcode once and reuses it. That makes String keys fast.
  • Security. Paths, addresses, and usernames travel as Strings. A changeable one could pass a security check, then be swapped for a bad value. Immutability closes that door.

One choice, many wins

Immutability is not one feature. It is a single design decision that quietly gives you safe sharing, thread safety, reliable map keys, a cached hashcode, and security all at once. That is why String works this way.

🧠 Reference vs contents

Separate two ideas: the variable and the String it points at.

  • A String variable does not hold the text. It holds a reference, a pointer to a String object in memory.
  • The text lives in that object.
  • When you reassign, you move the pointer to a different object. The old object’s contents never changed; you just stopped looking at it.

This program makes the difference clear.

public class RefVsContents {
public static void main(String[] args) {
String a = "Hi";
String b = a; // b points at the same String as a
a = a.concat("!"); // a now points at a NEW String "Hi!"
System.out.println("a = " + a); // Hi!
System.out.println("b = " + b); // Hi
}
}

The story:

  • a points at “Hi”. Then b = a makes b point at that same “Hi” object.
  • a = a.concat("!") builds a new “Hi!” and points a at it.
  • b was never touched, so it still points at the original “Hi”. The contents survived.

Output

a = Hi!
b = Hi

So “you can’t change a String” means the contents are frozen. You can always point a variable somewhere new. Two different things.

🐢 The cost: building strings in a loop

Immutability has one real cost.

  • Every + on a String creates a new String, it does not edit one.
  • Once is fine. Thousands of times in a loop and the work piles up.

This loop joins numbers with +. Each pass quietly makes a whole new String.

String result = "";
for (int i = 1; i <= 1000; i++) {
result = result + i + " "; // ❌ a brand new String every single pass
}

What each pass does:

  • result + i creates a new String, copies all the old characters in, adds the new piece, and discards the old one.
  • By pass 500 the text is long, so Java copies all of it again just to add a little.
  • The work grows as the text grows. Correct, but slow for large counts.

The fix is the StringBuilder, a changeable text buffer made for this. It keeps one buffer and extends it, with no repeated copying.

StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 1000; i++) {
sb.append(i).append(" "); // ✅ same buffer, no throwaway Strings
}
String result = sb.toString();

How this fixes it:

  • Each append adds to the same buffer. No new String per pass, no repeated copying.
  • One toString() at the end gives you the finished String.
  • For heavy building, this is the right tool. For a couple of joins, plain + is fine.

The simple rule

A few joins with + are fine and clear. Building text across many passes, especially in a loop, calls for a StringBuilder. Immutability is the reason the loop with + gets slow.

⚠️ Common Mistakes

The two classic immutability traps.

Calling a method and ignoring the result. The method ran, but you threw its return value away.

String s = "hello world";
// ❌ does nothing useful: the new String is thrown away
s.toUpperCase();
s.replace(' ', '_');
System.out.println(s); // still "hello world"
// ✅ catch the result by reassigning
s = s.toUpperCase();
s = s.replace(' ', '_');
System.out.println(s); // HELLO_WORLD

The first block returns new Strings that are immediately lost, so s cannot change. The fixed block stores each result back into s, so the changes stick.

Building strings with + in a big loop. Correct, but slow, because immutability forces a new String every pass.

// ❌ slow for large counts: a new String each pass
String out = "";
for (int i = 0; i < 100000; i++) out = out + i;
// ✅ fast: one buffer, then convert once
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++) sb.append(i);
String out = sb.toString();

The + version copies the whole growing text every pass. The StringBuilder version edits one buffer and makes a String only at the end.

The silent bug

A String method that returns a value but goes unused is silent. The code compiles and runs, it just does not do what you meant. Whenever you call a String method to transform text, assign the result somewhere.

✅ Best Practices

Habits for working with immutable Strings.

  • Always use the return value. A String method gives you a new String. Store it, usually with s = s.method().
  • Reassign to “change” text. You move the variable to the new String; the old contents never change.
  • Use StringBuilder for heavy building. Many passes in a loop call for a buffer, not +.
  • Trust String as a map key. Because it cannot change, it is a safe and fast HashMap key.
  • Lean on thread safety. Many threads can read the same String with no locks and no risk.
  • Keep + for simple joins. A couple of joins are clear and fast enough; do not over-engineer.

🧩 What You’ve Learned

Great work. Let’s recap immutable Strings.

  • Immutable means a String’s contents can never change once it is made.
  • ✅ Methods like concat, toUpperCase, and replace return a new String and leave the original untouched.
  • ✅ To keep a result you must reassign, like s = s.toUpperCase().
  • ✅ A variable holds a reference, not the text; reassigning moves the pointer, it does not edit contents.
  • ✅ String is immutable for safe sharing, thread safety, reliable map keys, a cached hashcode, and security.
  • ✅ The cost is that + in a loop makes a new String each pass; use a StringBuilder there.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does it mean that a String is immutable?

    Why: Immutable means the contents of a String object are fixed once it is created.

  2. 2

    What does name.toUpperCase() do to the variable name on its own?

    Why: It returns a new uppercase String. Unless you reassign it, name is unchanged.

  3. 3

    Which is a reason String is immutable?

    Why: Immutability gives safe sharing in the pool, thread safety, reliable map keys, a cached hashcode, and security.

  4. 4

    Why can building text with + in a big loop be slow?

    Why: Because String cannot change, each + creates a new String and copies the old text; StringBuilder avoids this.

🚀 What’s Next?

You now know why a String never changes and how to work with that. Next up: regular expressions, a tool for matching patterns inside text. They let you search, validate, and pull pieces out of Strings with short, precise patterns.

Java Regular Expressions

Share & Connect