Skip to content

Strings Interview Questions & Answers

24 questions Updated 2026-06-20 Share:

Java String interview questions — String immutability, the string pool and intern(), == vs equals, String vs StringBuilder vs StringBuffer, concatenation performance, and common String API methods.

Read the in-depth guideJava Strings — Immutability, the String Pool & StringBuilder Explained(opens in new tab)
24 of 24

A String wraps a private final array of characters that is never exposed or modified after construction; every "modifying" method returns a new String. Immutability buys four things interviewers want to hear:

  • Security — a path, URL, or username can't be changed after a check.
  • Caching / pooling — identical literals can be shared safely.
  • Thread safety — an immutable object needs no synchronization.
  • Hashcode caching — the hash is computed once and can never go stale, making String an ideal HashMap key.
String s = "hello";
s.concat(" world");      // returns a NEW String, discarded here
System.out.println(s);   // "hello" — s itself is unchanged
s = s.toUpperCase();     // reassign the reference to the new String

The trade-off is garbage: heavy string building creates throwaway objects, which is exactly why StringBuilder exists.

The string pool (a.k.a. the intern pool) is a special area where the JVM stores one shared copy of each distinct string literal. Two identical literals refer to the same pooled object, saving memory. Strings built with new are not pooled — they create a fresh heap object.

String a = "hi";              // pooled
String b = "hi";              // same pooled object
String c = new String("hi");  // new heap object, NOT pooled
a == b   // true  — same pooled reference
a == c   // false — different objects

Immutability is what makes pooling safe — since no one can mutate "hi", it's harmless to share it everywhere. Since Java 7 the pool lives on the heap (not PermGen), so it's subject to normal garbage collection.

A literal is resolved at class-load time and points at the pooled object — repeated literals share one instance. new String("x") forces a brand-new object on the heap plus puts "x" in the pool, so it can create two objects and is almost always wasteful.

String a = "x";            // 1 pooled object
String b = "x";            // reuses the pooled object
String c = new String("x"); // 1 new heap object (+ "x" already pooled)
a == b          // true
a == c          // false — c is a distinct object
a.equals(c)     // true  — same characters

Rule: never write new String("literal") — it defeats pooling for no benefit.

intern() returns the canonical pooled copy of a string: if an equal string is already in the pool, you get that reference; otherwise the string is added to the pool and that reference is returned. It lets you force == to work on computed strings.

String a = "hi";
String c = new String("hi");
a == c            // false
a == c.intern()   // true — intern() returns the pooled "hi"

String built = ("h" + new String("i")).intern();
a == built        // true

Use it sparingly: interning huge numbers of unique strings just fills the pool and can hurt more than it helps. It's mainly useful for deduplicating a bounded set of repeated values to save memory.

== compares references (do both variables point to the same object?), while .equals() compares the characters. For strings you almost always want .equals()== only "works" by accident when both operands happen to be the same pooled literal.

String a = "hi";
String b = new String("hi");
a == b         // false — different objects
a.equals(b)    // true  — same characters

String c = "hi";
a == c         // true  — both the pooled literal (coincidence, don't rely on it)

This is the single most common Java beginner bug. Always compare string content with .equals() (or equalsIgnoreCase).

Calling .equals() on a null reference throws NullPointerException. The classic trick is to put the known constant on the left, or use the null-safe Objects.equals.

String input = maybeNull();

input.equals("yes");          // NPE if input is null
"yes".equals(input);          // safe — false when input is null

Objects.equals(input, "yes"); // safe both ways

The "Yoda condition" ("yes".equals(input)) is the idiomatic fix; reach for Objects.equals(a, b) when either side could be null.

Type Mutable? Thread-safe? Use when
String no yes (immutable) fixed text, map keys
StringBuilder yes no building strings (the default)
StringBuffer yes yes (synchronized) shared across threads (rare)

StringBuilder and StringBuffer share the same API; the only difference is StringBuffer's methods are synchronized, which makes it slower.

StringBuilder sb = new StringBuilder();
sb.append("a").append(1).append(true); // fluent, mutates in place
String result = sb.toString();         // "a1true"

Prefer StringBuilder by default; you rarely need StringBuffer because shared mutable string state across threads is uncommon (and usually better handled with a local builder per thread).

Because String is immutable, s += part allocates a new String (and copies all existing characters) on every iteration — overall O(n²) work and a pile of garbage. A single StringBuilder reuses one growing buffer for amortized O(n).

// O(n²) — a new String each iteration
String r = "";
for (String p : parts) r += p;

// O(n) — one reused buffer
StringBuilder sb = new StringBuilder();
for (String p : parts) sb.append(p);
String r2 = sb.toString();

The compiler optimizes a single a + b + c expression into StringBuilder calls automatically — the problem is concatenation inside a loop, which it cannot collapse.

Historically javac rewrote a + b + c into a chain of new StringBuilder().append(a).append(b).append(c).toString(). Since Java 9, it instead emits an invokedynamic call bound to StringConcatFactory ("indified string concatenation"), letting the JVM pick the optimal strategy at runtime.

String s = "x" + n + "y";
// pre-9 : new StringBuilder().append("x").append(n).append("y").toString()
// 9+    : invokedynamic -> StringConcatFactory.makeConcatWithConstants(...)

Practical takeaway is unchanged: a single + expression is fine and fast; a += accumulation inside a loop still creates one object per iteration — use StringBuilder there.

  • equals — case-sensitive content equality, returns boolean.
  • equalsIgnoreCase — same, but ignores case.
  • compareTolexicographic ordering, returns an int: negative if this string sorts before the argument, 0 if equal, positive if after.
"abc".equals("ABC")            // false
"abc".equalsIgnoreCase("ABC")  // true
"apple".compareTo("banana")    // negative (a < b)
"abc".compareTo("abc")         // 0
"abc".compareToIgnoreCase("ABC") // 0

Use equals/equalsIgnoreCase for "are these the same?" and compareTo (or String.CASE_INSENSITIVE_ORDER) when sorting.

substring(begin) returns from begin to the end; substring(begin, end) returns [begin, end)end is exclusive. Indices out of range throw StringIndexOutOfBoundsException.

"interview".substring(0, 5);  // "inter" — chars 0..4
"interview".substring(5);     // "view"
"abc".substring(3);           // ""  (begin == length is legal)
"abc".substring(1, 1);        // ""  (begin == end)
"abc".substring(2, 1);        // exception — end < begin

Modern note: since Java 7u6 substring copies the chars into a new array (older JVMs shared the backing array, which could leak the whole parent string), so there's no longer a hidden memory-retention trap.

  • charAt(i) — the char at index i (0-based); out of range throws StringIndexOutOfBoundsException.
  • indexOf(x) — first index of a char or substring, or -1 if absent.
  • contains(seq)boolean, true if the substring occurs.
String s = "banana";
s.charAt(1);          // 'a'
s.indexOf('a');       // 1   (first match)
s.indexOf('a', 2);    // 3   (start searching from index 2)
s.lastIndexOf('a');   // 5
s.indexOf("xyz");     // -1  (not found)
s.contains("nan");    // true

Remember indexOf returns −1, not throws, when nothing matches — guard with if (i >= 0) before using the result as an index.

split(regex) breaks a string on a regular expression (not a literal!) and returns a String[]. Two surprises: it drops trailing empty strings by default, and regex metacharacters must be escaped.

"a,b,c".split(",")         // ["a", "b", "c"]
"a,b,,".split(",")         // ["a", "b"]  — trailing empties removed
"a,b,,".split(",", -1)     // ["a", "b", "", ""] — limit -1 keeps them
"a.b.c".split(".")         // [] — "." matches ANY char; needs "\\."
"a.b.c".split("\\.")       // ["a", "b", "c"]

Pass a negative limit to keep trailing blanks, and escape . | ( ) [ ] \ etc. For a plain delimiter use Pattern.quote(delim).

replace works on literal text (a char or CharSequence) and replaces every occurrence. replaceAll (and replaceFirst) take a regex as the pattern. Both return a new string — String is immutable.

"a.b.c".replace(".", "-")      // "a-b-c"  — literal dot
"a.b.c".replaceAll(".", "-")   // "-----"  — "." is regex "any char"!
"a.b.c".replaceAll("\\.", "-") // "a-b-c"  — escaped dot
"a1b2".replaceAll("\\d", "#")  // "a#b#"   — regex digit class

If you don't need regex, prefer replace — it's faster and avoids the "every char got replaced" trap.

Both remove leading and trailing whitespace, but trim (legacy) only strips characters <= U+0020 (ASCII space and control chars), while strip (Java 11+) is Unicode-aware and removes all Unicode whitespace. strip also has one-sided variants.

"  hi  ".trim()              // "hi"
" hi ".trim()      // " hi " — thin space NOT removed
" hi ".strip()     // "hi"             — Unicode-aware
" hi ".stripLeading()        // "hi "
" hi ".stripTrailing()       // " hi"

Prefer strip on modern Java for correct handling of non-ASCII whitespace; trim is fine for plain ASCII input.

isEmpty() is true only when the length is 0. isBlank() (Java 11+) is true when the string is empty or contains only whitespace.

"".isEmpty()      // true
"   ".isEmpty()   // false — it has characters
"   ".isBlank()   // true  — only whitespace
"x".isBlank()     // false

Neither is null-safe, so check for null first (or use a helper like StringUtils.isBlank). Use isBlank when "all-spaces" should count as "empty" — e.g. validating user input.

A String is immutable and may sit in the pool, so you cannot clear its contents — the password lingers in memory until garbage collected and could surface in a heap dump. A char[] can be explicitly wiped right after use.

char[] pw = readPassword();
try {
    authenticate(pw);
} finally {
    Arrays.fill(pw, '\0'); // overwrite — minimizes the exposure window
}

This is why JPasswordField.getPassword() and Console.readPassword() return char[]. Strings also risk accidental logging via toString(), which char[] avoids.

Use toCharArray() / the String(char[]) constructor for characters, and getBytes(charset) / new String(bytes, charset) for bytes. Always specify a charset for byte conversions to avoid platform-default surprises.

char[] chars = "hi".toCharArray();        // ['h','i']
String fromChars = new String(chars);      // "hi"
String fromChars2 = String.valueOf(chars); // "hi"

byte[] bytes = "hi".getBytes(StandardCharsets.UTF_8);
String fromBytes = new String(bytes, StandardCharsets.UTF_8); // "hi"

Omitting the charset uses the JVM default encoding, which differs across machines and causes the classic "works on my box" mojibake bug — pass StandardCharsets.UTF_8 explicitly.

Parse with the wrapper classes' static methods; produce text with String.valueOf or concatenation. parseXxx returns a primitive, valueOf a wrapper object.

int n     = Integer.parseInt("42");        // primitive int
double d  = Double.parseDouble("3.14");
Integer w = Integer.valueOf("42");          // Integer object
String s  = String.valueOf(42);             // "42"
String s2 = "" + 42;                         // "42" via concatenation

A malformed string throws NumberFormatException, so validate or try/catch when parsing untrusted input.

  • String.format builds a string from a printf-style template.
  • String.join glues elements with a delimiter (Java 8+).
  • repeat duplicates a string n times (Java 11+).
String.format("%s is %d", "Ada", 36);   // "Ada is 36"
String.format("%.2f", 3.14159);          // "3.14"
String.join("-", "a", "b", "c");          // "a-b-c"
String.join(",", List.of("x", "y"));      // "x,y"
"ab".repeat(3);                            // "ababab"

String.join is the clean way to build delimiter-separated output without a trailing-comma loop; format is handy but slower than direct concatenation for hot paths.

Text blocks (Java 15+) are multi-line string literals delimited by triple quotes """. They preserve line breaks and let you write JSON, SQL, or HTML without escaping quotes or concatenating lines.

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

Incidental leading whitespace is stripped relative to the closing delimiter's indentation, and the result is an ordinary String — it's pooled and supports every String method. Use \ at line end to suppress a newline.

Since Java 7 you can switch on a String. The compiler implements it in two steps: it switches on the string's hashCode() (an int switch), then guards each matched bucket with an .equals() check to handle hash collisions. So it is null-sensitive and case-sensitive.

switch (cmd) {
    case "start" -> run();
    case "stop"  -> halt();
    default      -> usage();
}
// a null cmd throws NullPointerException — switch calls cmd.hashCode()

Because matching is by equals, the labels are exact and case-sensitive; normalize with toLowerCase() first if you need case-insensitive matching.

String.hashCode() uses the polynomial formula s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1], accumulated in an int. Because the string is immutable, the result is computed once and stored in a private int hash field, then returned on every later call.

"Aa".hashCode()  // 2112
"BB".hashCode()  // 2112 — famous collision (same hash, not equal)

The constant 31 is an odd prime that the JVM can optimize to (h << 5) - h. Caching is safe precisely because the contents can never change — one of the concrete payoffs of immutability and why String is the go-to HashMap key.

Classic options are an indexed loop with charAt, or toCharArray() with a for-each. Java 8 added chars(), an IntStream of code units, for a functional style.

String s = "abc";
for (int i = 0; i < s.length(); i++) {   // indexed
    char c = s.charAt(i);
}
for (char c : s.toCharArray()) { }        // for-each over a copy

long vowels = s.chars()                    // IntStream of code units
    .filter(c -> "aeiou".indexOf(c) >= 0)
    .count();

chars() yields int code units; for full Unicode (emoji / supplementary characters) use codePoints() instead, which combines surrogate pairs.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel