Lesson 03 · 10 min read · Java for placements

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);          // HELLO

String 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:

  1. 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.
  2. Thread safety for free. An immutable object cannot be observed half-modified, so strings need no synchronisation.
  3. Safe as a hash key. hashCode can be computed once and cached, and a key that cannot change can never become unfindable in a HashMap.

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);          // true

So 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);          // true

The 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 points

Two 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)          // StringIndexOutOfBoundsException

Converting 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') // 7

A 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

  • String is immutable; every method returns a new one.
  • Immutability enables the pool, gives thread safety, and lets hashCode be 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²) — use StringBuilder.
  • StringBuilder is not synchronised, StringBuffer is. Prefer StringBuilder.
  • split takes a regex, so "." must be escaped as "\\.".
  • substring's end index is exclusive and out of range throws.
Lesson 3 of 12 · all 12 are free to read, no account. Course contents