Technology  /  Java

โ˜• Java 15 guides ยท updated 2026

From the JVM to streams, generics, and concurrency โ€” the language behind most enterprise backends, taught with modern Java in mind.

Java Strings Deep Dive: Immutability, the String Pool, and StringBuilder

Strings are the most-used object type in almost every Java codebase โ€” and one of the most misunderstood. Why does == sometimes work on strings and sometimes not? Why does everyone warn against + in loops? What actually happens when you โ€œmodifyโ€ a string?

All three questions have the same answer at their root: String is immutable. Once you really get that, the rest of Javaโ€™s string behavior stops being trivia and starts being obvious.


Immutability: The Design Decision Everything Else Follows From

A Java String can never be changed after creation. Every method that looks like it modifies a string actually returns a new string:

String name = "java";
name.toUpperCase();
System.out.println(name); // "java" โ€” unchanged!
name = name.toUpperCase();
System.out.println(name); // "JAVA" โ€” reassigned to a new object

Calling toUpperCase() and discarding the result is a classic beginner bug โ€” the IDE will usually warn โ€œresult of toUpperCase is ignored,โ€ a warning worth respecting.

Why did Javaโ€™s designers make strings immutable? Three practical reasons:

  1. Safety. Strings are used as map keys, file paths, class names, and security identifiers. If a string could mutate after being used as a HashMap key, the map would silently corrupt (the keyโ€™s hash would no longer match its bucket). Immutability makes strings trustworthy.
  2. Thread-safety for free. Immutable objects can be shared across threads without locks. In server code handling thousands of concurrent requests, that matters enormously.
  3. The string pool โ€” which deserves its own section.

The String Pool (and Why == Lies to You)

Because strings canโ€™t change, the JVM can safely share them. Identical string literals in your code all point to a single pooled object:

code heap
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
String a = "cat"; โ”€โ”€โ”
โ”œโ”€โ”€โ–ถ "cat" (string pool)
String b = "cat"; โ”€โ”€โ”˜
String c = new String("cat"); โ”€โ”€โ–ถ "cat" (separate object, outside pool)

Which explains this classic:

String a = "cat";
String b = "cat";
String c = new String("cat");
System.out.println(a == b); // true โ€” same pooled object
System.out.println(a == c); // false โ€” different objects
System.out.println(a.equals(c)); // true โ€” same characters

== compares references. Literals share a reference, so == appears to work โ€” until a string arrives from user input, a file, or concatenation at runtime, and suddenly it doesnโ€™t. The bug hides in testing (where you compare literals) and detonates in production.

The rule, permanently: compare string contents with .equals(), or .equalsIgnoreCase() when case doesnโ€™t matter. And when the string might be null, put the literal first โ€” "yes".equals(input) canโ€™t throw a NullPointerException; input.equals("yes") can.

(Also: never write new String("cat") โ€” it creates a pointless duplicate of an already-pooled string. Its main use in the wild is interview questions.)


The Methods Youโ€™ll Actually Use

String has dozens of methods; these cover 95% of real work:

String s = " Hello, Java World ";
s.length() // 21 (includes spaces)
s.trim() // "Hello, Java World" โ€” or strip(), which is Unicode-aware
s.toLowerCase() // " hello, java world "
s.contains("Java") // true
s.startsWith(" He") // true
s.indexOf("Java") // 9 (-1 if absent)
s.replace("Java", "Java 21") // replaces ALL occurrences (plain text, not regex)
s.substring(2, 7) // "Hello" โ€” start inclusive, end exclusive
s.charAt(2) // 'H'
s.isBlank() // false โ€” true for empty or whitespace-only (Java 11+)
s.repeat(2) // doubles the string (Java 11+)

Splitting and joining:

String csv = "red,green,blue";
String[] parts = csv.split(","); // careful: split takes a REGEX
String joined = String.join(" | ", parts); // "red | green | blue"

The regex detail bites people: "a.b.c".split(".") returns an empty array, because . means โ€œany characterโ€ in regex. You want split("\\."). Same for |, *, +. If your split behaves bizarrely, this is why.

substringโ€™s half-open interval ([start, end)) follows the same convention as everything in Java โ€” substring(i, j) has length j - i, and s.substring(0, s.length()) is the whole string. Internalize half-open ranges once; theyโ€™re everywhere.


Concatenation, and Why + in a Loop Is a Performance Bug

For a handful of pieces, + is fine โ€” the compiler optimizes single-expression concatenation:

String msg = "Order " + orderId + " shipped to " + city; // perfectly fine

The trouble is loops. Since strings are immutable, each += builds a brand-new string, copying everything so far:

String report = "";
for (Order o : orders) {
report += o.summary() + "\n"; // copies the ENTIRE report every iteration
}

Thatโ€™s O(nยฒ) work โ€” 10,000 orders means roughly 50 million character copies. The fix is StringBuilder, a mutable character buffer:

StringBuilder report = new StringBuilder();
for (Order o : orders) {
report.append(o.summary()).append('\n');
}
String result = report.toString();

Linear time, one final string. The rule of thumb: + for fixed, small concatenations; StringBuilder whenever you build a string incrementally in a loop. (You may see StringBuffer in old code โ€” itโ€™s the synchronized, slower ancestor; effectively never needed today.)

Often the cleanest answer is neither โ€” itโ€™s String.join or a stream collector:

String result = orders.stream()
.map(Order::summary)
.collect(Collectors.joining("\n"));

Formatting

For anything with placeholders, String.format/formatted beats concatenation for readability:

String line = String.format("%-10s %8.2f %5d", name, price, qty);
String msg = "Hello, %s. You have %d alerts.".formatted(user, count); // Java 15+

%s string, %d integer, %.2f two-decimal float, %-10s left-pad to width 10. If youโ€™re aligning columns or formatting currency-ish output, this is the tool.


Text Blocks: Multi-Line Strings Without the Pain (Java 15+)

Before text blocks, embedding JSON or SQL in Java was an escape-character nightmare:

String json = "{\n \"name\": \"Ada\",\n \"role\": \"engineer\"\n}";

Text blocks fix it with triple quotes:

String json = """
{
"name": "Ada",
"role": "engineer"
}
""";

The compiler strips the common leading whitespace (determined by the least-indented line, including the closing """), so your Java indentation doesnโ€™t leak into the string. Quotes inside need no escaping. For SQL, JSON, HTML snippets, and test fixtures, text blocks are strictly better โ€” if youโ€™re on Java 15+, stop writing \n chains.


char, Unicode, and a Warning About length()

A Java char is a 16-bit UTF-16 code unit โ€” which is not the same as โ€œa characterโ€ in the human sense. Characters outside the Basic Multilingual Plane (most emoji, many historic scripts) occupy two chars:

String s = "Hi๐Ÿ‘‹";
System.out.println(s.length()); // 4, not 3
System.out.println(s.codePointCount(0, s.length())); // 3 โ€” actual code points

For everyday ASCII-ish text, length() and charAt() behave as expected. But if your application handles emoji or international text (it does), be careful about slicing strings at arbitrary indexes โ€” you can cut a character in half. When correctness matters, iterate code points (s.codePoints()) rather than chars. Most developers learn this the day an emoji corrupts their output; now you know early.


Practical Patterns Worth Memorizing

Null-safe emptiness check:

if (input == null || input.isBlank()) { ... } // order matters โ€” short-circuit

Reverse a string (interview staple โ€” note it needs StringBuilder, since String has no reverse):

String reversed = new StringBuilder(s).reverse().toString();

Count occurrences of a character:

long spaces = s.chars().filter(c -> c == ' ').count();

Case-insensitive comparison without allocating: a.equalsIgnoreCase(b) โ€” not a.toLowerCase().equals(b.toLowerCase()), which creates two throwaway strings and mishandles some locales (the Turkish dotless-ฤฑ problem is real and has broken real systems).

Parsing numbers out of strings โ€” see the data types guide; remember Integer.parseInt throws on bad input, so guard user-provided data.


How Strings Show Up in Interviews and Code Review

A short field report from both sides of the table, because strings are disproportionately represented in both settings.

Interview patterns built on this chapter. Reverse a string (StringBuilder, or a two-pointer char array swap), check for palindromes (two pointers meeting in the middle), find the first non-repeating character (one pass to count with an array or map, second pass to find), group anagrams (sort each wordโ€™s characters to form a canonical key for a HashMap), and string compression (โ€œaaabbโ€ โ†’ โ€œa3b2โ€ โ€” StringBuilder plus a run counter). Every one of them is testing the same two things: do you know strings are immutable (so you build results in a StringBuilder or array, not by repeated concatenation), and can you index into a string without off-by-one errors. If you can implement substring semantics from memory โ€” start inclusive, end exclusive โ€” the index questions mostly solve themselves.

Code review patterns. The recurring string comments in real pull requests, in rough order of frequency: == where .equals() was meant; concatenation in a loop (flagged by any decent static analyzer these days); split() with an unescaped regex metacharacter; manual newline concatenation where a text block would be clearer; and toLowerCase().equals(...) where equalsIgnoreCase is both faster and locale-safer. None of these are exotic โ€” theyโ€™re the everyday friction of the language, which is exactly why internalizing them early pays off daily.

A note on performance intuition. Developers new to Java sometimes micro-optimize strings in the wrong places โ€” interning everything, avoiding + even for two operands, pre-sizing every StringBuilder. Modern JVMs make simple concatenation of a few pieces essentially free (the compiler emits an optimized construction, not naive copying). The only string performance rule that matters in application code is the loop rule: incremental building goes through StringBuilder or a stream collector. Save the rest of your optimization budget for I/O and algorithms, where it buys something measurable.

Frequently Asked Questions

How much memory does a String use? Since Java 9โ€™s compact strings, Latin-1-only strings store one byte per character; strings containing any character beyond Latin-1 use two. Add roughly 40 bytes of object overhead (header, length, coder flag, array header). For most applications this is trivia โ€” until you hold millions of strings in a cache, at which point deduplication (-XX:+UseStringDeduplication with G1) and interning strategies become real levers.

What does intern() do, and should I use it? It returns the pooled instance of a stringโ€™s content, adding it to the pool if absent โ€” manually extending what literals get automatically. Legitimate uses are rare (massive datasets with heavy value repetition, where identity comparison becomes profitable); casual interning adds pool pressure and complexity for nothing. If youโ€™re reaching for intern() to make == work, the actual fix is .equals().

Why is String final? So no subclass can override its methods to mutate behavior โ€” which would break the pool (shared instances must be safe to share), break HashMap keys, and break every security check that validates a string then uses it. Immutability guarantees are only as strong as the classโ€™s inability to be subverted; final closes the subclassing door. Itโ€™s a design lesson in miniature: the immutability guide generalizes it.

String.format vs concatenation vs text blocks โ€” which for building SQL? None of them, for values: parameterized queries (PreparedStatement placeholders) are non-negotiable, because string-built SQL is how injection vulnerabilities happen. Text blocks are excellent for the static SQL skeleton; parameters carry the data. The same principle applies to HTML and shell commands โ€” structure via strings, data via safe channels.

Are Java strings UTF-8? In memory theyโ€™re conceptually UTF-16 (with the compact-strings optimization underneath); UTF-8 enters when you encode for the outside world: s.getBytes(StandardCharsets.UTF_8). Always name the charset explicitly at I/O boundaries โ€” the platform-default overloads have caused decades of mojibake, which is why Java 18 finally made UTF-8 the default everywhere.

Self-Check

Predict, then verify:

String a = "ja" + "va";
String b = "java";
String c = new StringBuilder("ja").append("va").toString();
System.out.println(a == b); // ?
System.out.println(b == c); // ?
System.out.println(b.equals(c)); // ?
System.out.println("A.B.C".split(".").length); // ?
String s = "hello";
s.concat(" world");
System.out.println(s); // ?

Answers: true (compile-time constant folding pools "ja" + "va" as "java"), false (runtime-built string, not pooled), true, 0 (regex dot!), hello (immutability โ€” the concat result was discarded).

Five for five means you genuinely understand Java strings โ€” pool, immutability, and the regex trap included. Next up, the series moves into Classes and Objects, where you stop using Javaโ€™s types and start designing your own.