Strings in Java: How They Actually Work in Memory
Table of Contents
Here's a question that trips up even developers with a few years of
experience: if you concatenate strings inside a loop 10,000 times, why does
your code suddenly get slow — even though each individual + looks
like a tiny, cheap operation? The answer is the single most important thing to
understand about strings in Java, and it comes down to one word:
immutability.
We covered Arrays last time as contiguous memory you can freely change. Strings look similar on the surface — they're basically a sequence of characters — but Java treats them very differently under the hood, and that difference is exactly what today's post is about.
A String is Secretly a char Array
At the lowest level, a Java String is backed by a character
array (technically a byte array since Java 9's "compact strings," but the
concept is the same). So why not just use char[] directly? Because
String adds one critical guarantee on top: once created, its
contents can never change.
Why Strings Are Immutable
When you write code like this:
String greeting = "Hello";
greeting = greeting + " World";
It looks like you modified greeting in place. You didn't. Java
created a brand new String object containing "Hello World" and
pointed the greeting variable at it. The original
"Hello" object still exists in memory — untouched — until the
garbage collector eventually cleans it up.
Immutability isn't a limitation — it's a deliberate design choice. It makes strings safe to share across multiple parts of a program (and multiple threads) without one part accidentally corrupting another's data. It's also what makes the String Pool (next section) possible at all.
The String Pool: Java's Memory-Saving Trick
Because strings can't change, Java can safely reuse identical string literals instead of creating duplicates. This shared storage area is called the String Pool:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — both point to the SAME pooled object
String c = new String("hello");
System.out.println(a == c); // false — new String() forces a new object,
// bypassing the pool entirely
System.out.println(a.equals(c)); // true — same characters, compared properly
This is one of the most common interview traps in Java: ==
compares whether two variables point to the exact same object in memory.
.equals() compares whether the actual characters match. For
strings, you almost always want .equals() — using
== can pass in quick tests and then silently fail in production
the moment a string doesn't come from a literal.
Why String Concatenation in a Loop Gets Slow
Now the loop problem from the intro makes sense. Every time you do
result = result + something, Java throws away the old String
object and builds an entirely new one, copying all the old characters plus the
new ones. Do that inside a loop, and each iteration copies a little more than
the last:
// SLOW — O(n²). Each concatenation copies the entire string built so far.
String result = "";
for (int i = 0; i < 10000; i++) {
result = result + i; // creates a brand new String object every time
}
The fix is StringBuilder — a mutable character buffer designed
exactly for this situation:
// FAST — O(n). StringBuilder grows in place, no repeated copying.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString(); // convert to String only once, at the end
Rule of thumb: a handful of concatenations outside a loop? Plain
String is fine and more readable. Building a string
inside a loop — especially one that could run many times?
StringBuilder, every time.
Common String Operations and Their Real Complexity
| Operation | Time Complexity | Why |
|---|---|---|
charAt(i) | O(1) | Direct index into the backing array |
length() | O(1) | Cached when the String is created |
substring() | O(n) | Copies the relevant characters into a new String |
concat() / + | O(n) | Creates a new String, copying both originals |
equals() | O(n) | Must compare characters until a mismatch or the end |
StringBuilder.append() | O(1) amortized | Grows an internal buffer in place |
A Classic String Problem: Checking a Palindrome
Let's put this together in code you'll genuinely see in interviews — checking whether a string reads the same forwards and backwards:
public class PalindromeCheck {
public static boolean isPalindrome(String s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false; // mismatch found — not a palindrome
}
left++;
right--;
}
return true;
}
public static void main(String[] args) {
System.out.println(isPalindrome("racecar")); // true
System.out.println(isPalindrome("hello")); // false
}
}
Notice the technique: two pointers moving toward each other from opposite ends. It's O(n) time and O(1) extra space — no new string is built, just two index variables doing all the work. This "two-pointer" pattern shows up constantly once you get into searching and sorting, which is exactly where we're headed next.
Common Interview Questions
- Why are strings immutable in Java, and what problems would mutable strings cause?
- What's the difference between
==and.equals()for strings, and when would relying on==actually break? - Why is building a string inside a loop with
+considered bad practice? What's the fix? - How would you reverse a string without using a built-in reverse method?
The Takeaway
Strings behave like arrays in some ways and completely differently in
others — the immutability is the whole story, and it explains everything else:
why == misleads people, why the String Pool exists, and why
StringBuilder is a tool you'll reach for constantly. With Arrays
and Strings both covered, you now have the two building blocks that almost
every algorithm in this series operates on. Next up: Linear Search — the
simplest possible way to find something, and the baseline every faster search
algorithm after it will be measured against.
Happy learning!
— Team CodeElevateX 🚀
Comments
Post a Comment