Strings, the string pool and StringBuilder
Why strings are immutable, what the string pool is and how it makes == misleading, when StringBuilder is required, and the methods worth knowing cold.
If a Java interview asks you one question, it is this one. Get it complete and you have set the tone for the rest of the round.
Strings are immutable
String s = "hello";
s.toUpperCase();
System.out.println(s); // hello — unchanged
s = s.toUpperCase();
System.out.println(s); // HELLOString has no method that modifies it. Every method returns a new String. If you are not
assigning the result, nothing happened.
Why immutable? Three reasons, and giving all three is a complete answer:
- The string pool depends on it. Strings are shared between references (see below); if one holder could modify a shared string, every other holder would see the change.
- Thread safety for free. An immutable object cannot be observed half-modified, so strings need no synchronisation.
- Safe as a hash key.
hashCodecan be computed once and cached, and a key that cannot change can never become unfindable in aHashMap.
That third one is worth expanding if you get the chance: String caches its hash code in a field,
which is only sound because the contents cannot change.
The string pool
The JVM keeps a pool of string literals in the heap. Two identical literals are the same object:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — both point at the pooled literal
String c = new String("hello");
System.out.println(a == c); // false — new String() forces a new object
System.out.println(a.equals(c)); // true — same characters
String d = c.intern(); // put it in the pool / get the pooled one
System.out.println(a == d); // trueSo new String("hello") creates two objects: the pooled literal and a separate heap object.
That is why you should never write it.
Concatenation of literals is folded at compile time, which produces another surprise:
String e = "hel" + "lo"; // folded by javac into the literal "hello"
System.out.println(a == e); // true
String part = "hel";
String f = part + "lo"; // computed at RUN time — a new object
System.out.println(a == f); // false
final String cpart = "hel";
String g = cpart + "lo"; // a compile-time constant again
System.out.println(a == g); // trueThe rule
Always compare strings with
equals, never with==.==asks whether two references point at the same object, which is an implementation detail of pooling and not what you meant.
String input = scanner.nextLine();
if (input == "yes") { } // essentially always false
if (input.equals("yes")) { } // right
if ("yes".equals(input)) { } // right, and null-safe
if ("yes".equalsIgnoreCase(input)) { }The third form — literal first — cannot throw a NullPointerException when input is null. It is
a small habit that removes a whole class of crash.
StringBuilder
Because strings are immutable, += in a loop allocates a new string on every iteration:
// O(n²) — each += copies everything built so far
String out = "";
for (String word : words) {
out += word;
}
// O(n) — one growable buffer
StringBuilder sb = new StringBuilder();
for (String word : words) {
sb.append(word);
}
String out = sb.toString();For ten words this does not matter. For a hundred thousand it is the difference between passing and timing out — and interviewers do ask you to spot it in code they show you.
StringBuilder is mutable and has the methods String lacks:
StringBuilder sb = new StringBuilder("hello");
sb.append(" world"); // hello world
sb.insert(0, ">> "); // >> hello world
sb.reverse(); // dlrow olleh >>
sb.deleteCharAt(0);
sb.setCharAt(0, 'X');
sb.replace(0, 1, "Y");
System.out.println(sb.length());
String result = sb.toString();sb.reverse() is how you reverse a string in Java — there is no String.reverse().
String reversed = new StringBuilder(s).reverse().toString();StringBuilder versus StringBuffer
| Mutable | Thread-safe | Speed | |
|---|---|---|---|
String |
no | yes (immutable) | — |
StringBuilder |
yes | no | faster |
StringBuffer |
yes | yes (synchronised) | slower |
StringBuffer is the older class; every method is synchronized. StringBuilder was added in
Java 5 as the same API without the locking. Use StringBuilder unless multiple threads share the
buffer, which in a coding round they never do.
Note that a single a + b outside a loop is fine — the compiler already turns it into a
StringBuilder (or an invokedynamic concat on modern JDKs). It is only the loop that is
quadratic, because a fresh builder is created on each iteration.
The methods worth knowing cold
String s = "Placement Season 2027";
s.length() // 21
s.charAt(0) // 'P'
s.indexOf("Season") // 10, or -1 if absent
s.lastIndexOf('e') // last position
s.contains("Season") // true
s.substring(10) // "Season 2027"
s.substring(10, 16) // "Season" — end exclusive
s.toUpperCase() s.toLowerCase()
s.trim() // both ends, whitespace
s.strip() // Java 11+, Unicode-aware
s.isEmpty() // length == 0
s.isBlank() // Java 11+, empty or only whitespace
s.replace('e', 'E') // every occurrence
s.split(" ") // String[] — takes a REGEX
s.startsWith("Place") s.endsWith("2027")
s.compareTo("A") // <0, 0 or >0 — lexicographic
s.toCharArray() // char[]
String.join(", ", "a", "b") // "a, b"
String.valueOf(42) // "42"
" x ".chars().count() // stream of code pointsTwo traps in that list.
split takes a regular expression, not a literal:
"a.b.c".split(".") // an array of empty strings — "." matches anything
"a.b.c".split("\\.") // ["a", "b", "c"] — escaped
"a|b".split("\\|") // same for | + * ? ( ) [ ] { } ^ $substring end index is exclusive, and out of range throws:
"hello".substring(1, 3) // "el"
"hello".substring(2) // "llo"
"hello".substring(6) // StringIndexOutOfBoundsExceptionConverting between strings and numbers:
int n = Integer.parseInt("42"); // int; NumberFormatException on bad input
Integer boxed = Integer.valueOf("42"); // Integer
String t = String.valueOf(42); // "42"
String u = Integer.toString(42); // "42"
String v = 42 + ""; // works, but do not
String bin = Integer.toBinaryString(10); // "1010"Character work
For per-character logic, Character's static methods are the readable option:
Character.isLetter('a') Character.isDigit('7')
Character.isLetterOrDigit('_') Character.isWhitespace(' ')
Character.isUpperCase('A') Character.toLowerCase('A')
Character.getNumericValue('7') // 7A palindrome check, put together:
public static boolean isPalindrome(String s) {
StringBuilder clean = new StringBuilder();
for (char c : s.toCharArray()) {
if (Character.isLetterOrDigit(c)) {
clean.append(Character.toLowerCase(c));
}
}
String forward = clean.toString();
return forward.equals(clean.reverse().toString());
}And the O(1)-space version, which is what a follow-up asks for — two pointers, no builder:
public static boolean isPalindromeTwoPointer(String s) {
int i = 0, j = s.length() - 1;
while (i < j) {
while (i < j && !Character.isLetterOrDigit(s.charAt(i))) i++;
while (i < j && !Character.isLetterOrDigit(s.charAt(j))) j--;
if (Character.toLowerCase(s.charAt(i)) != Character.toLowerCase(s.charAt(j))) {
return false;
}
i++;
j--;
}
return true;
}What to take into an interview
Stringis immutable; every method returns a new one.- Immutability enables the pool, gives thread safety, and lets
hashCodebe cached. - Identical literals share one pooled object;
new String("x")forces a second object. - Compare with
equals, never==. Put the literal first for null safety. +=in a loop is O(n²) — useStringBuilder.StringBuilderis not synchronised,StringBufferis. PreferStringBuilder.splittakes a regex, so"."must be escaped as"\\.".substring's end index is exclusive and out of range throws.