Strings basics: interview questions & scenarios
These are the String questions I see in almost every Java screen, from the one-liner warm-ups to the follow-ups that tell me whether you've debugged real string bugs. They cover the String API, == vs equals(), the string pool, and console input and output.
Here's how to use this page: read the question, answer it out loud first, then open the model answer. Don't skip the "What if…" chains. That's where most candidates run out of road, and where you can show real depth.
What's the difference between == and equals() for Strings?
Tests reference vs content equality
Show answerHide answer
Model answer
== compares references: are these the same object? equals() compares content: do they hold the same characters? Two Strings with identical text can be different objects, so == can be false while equals() is true. For strings you should always use equals() (or Objects.equals when either side can be null).
How I’d say it in the interview
"Double-equals checks identity, equals checks the characters. It sometimes looks like == works because literals are interned, but strings from I/O or concatenation at runtime are new objects, so I always use equals."
Signals depth
Explaining why == sometimes works (the string pool) and that the bug hides in tests because test data is usually literals.
Red flags
"== compares values for primitives and Strings." Or saying equals is slower so == is a valid optimisation.
Follow-ups interviewers ask next
What if both strings come from literals in different classes?
Then
==istrue. The JLS guarantees that identical literals refer to the same interned instance, even across classes and packages, as long as they're loaded by the same JVM.What if you know both sides are literals? Why not use == then?
Because "I know both are literals" is a fact about today's code. The next refactor reads one from config, and the comparison silently breaks with no compile error.
equals()already short-circuits on identical references, so you lose nothing.
What does this print?
Tests constant folding, string pool
Show answerHide answer
Model answer
String s1 = "ab";
String s2 = "a" + "b";
String a = "a";
String s3 = a + "b";
final String fa = "a";
String s4 = fa + "b";
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1 == s4); // true
System.out.println(s1 == s3.intern()); // true
"a" + "b" is a constant expression, so javac folds it into the literal "ab", which is pooled. fa is a constant variable (final, initialised with a constant), so fa + "b" is folded too. a isn't final, so a + "b" is built at runtime with invokedynamic and is a new object. intern() returns the pooled copy.
How I’d say it in the interview
"The compiler folds concatenations of compile-time constants into a single pooled literal, including final variables initialised with constants. Anything with a regular variable is built at runtime and is a new object."
Signals depth
Using the term constant variable, and mentioning that javap -c shows ldc "ab" for the folded cases and makeConcatWithConstants for the runtime one.
Red flags
"final makes == work." It's about compile-time constants, not the keyword.
Follow-ups interviewers ask next
What if fa is declared as final String fa = getA();?
Then
s1 == s4isfalse.fais final but its initializer isn't a constant expression, so it's not a constant variable and the concatenation happens at runtime.
How many objects does new String("hello") create?
Tests string pool, object allocation
Show answerHide answer
Model answer
One or two String objects. The literal "hello" refers to a pooled String, created when the literal is first resolved if it isn't already in the pool. Then new String(...) always allocates one more String on the heap. Since Java 9 the String(String) constructor reuses the original's byte[], so the two String objects share one array.
How I’d say it in the interview
"At most two: the interned literal, if nobody has used 'hello' before, and the new object that new always creates. They share the same backing byte array."
Signals depth
Saying "if not already present" and mentioning the shared byte[]. Also pointing out there's no reason to write new String("literal") in real code.
Red flags
A confident "always two" or "always one" with no explanation.
Follow-ups interviewers ask next
What if the line runs inside a loop 1,000 times?
The literal is resolved once and reused, so you get 1,000 new String objects plus the single pooled one. That's 1,000 pointless allocations, which is why linters flag
new String(String).
Why is String immutable?
Tests design rationale
Show answerHide answer
Model answer
Four reasons, and they reinforce each other:
- Security: paths, URLs, class names and credentials are Strings; if they could change after a security check, the check would be useless.
- The string pool: literals can be shared JVM-wide only because nobody can modify them.
- Hash caching: the hash is computed once and stored, which makes Strings cheap
HashMapkeys. - Thread safety: immutable objects can be shared across threads with no synchronization.
The class is also final, so no subclass can add mutable behaviour.
How I’d say it in the interview
"Immutability makes strings safe to share: across threads, in the pool and as map keys. It also protects security checks on things like file paths, and it lets String cache its hash code."
Signals depth
Connecting immutability to final on the class, and noting that a mutated HashMap key would land in the wrong bucket and become unreachable.
Follow-ups interviewers ask next
What if you could change a String used as a HashMap key?
Its hash would change but the entry would stay in the bucket for the old hash.
get()with the new content looks in the wrong bucket, andget()with the old content finds the bucket but failsequals(). The entry is effectively lost: a memory leak and a logic bug.
What does "a.b.c".split(".") return?
Tests regex arguments, trailing empty strings
Show answerHide answer
Model answer
An empty array. split takes a regex, and . matches any character, so every character is a delimiter and every piece is an empty string. Trailing empty strings are removed by default, which removes all of them. Use split("\\.") or split(Pattern.quote(".")) to get [a, b, c].
How I’d say it in the interview
"Split takes a regex, so a dot means 'any character'. Everything becomes an empty token and the trailing ones are dropped, so the array is empty. You escape it or use Pattern.quote."
Signals depth
Also mentioning "a,b,,".split(",") returns 2 elements and that split(",", -1) keeps trailing empties; and that "|" has the same trap.
Red flags
"It returns [a, b, c]."
Follow-ups interviewers ask next
What if you call it on a hot path with "\\s+"?
splitonly skips the regex engine for a single literal character (or a backslash plus a non-alphanumeric character)."\\s+"compiles a newPatternon every call. Hoist it:private static final Pattern WS = Pattern.compile("\\s+");and useWS.split(line).
Why does this loop get slow as n grows, and how do you fix it?
Tests StringBuilder, invokedynamic concat
Show answerHide answer
Model answer
String result = "";
for (String word : words) {
result += word + " ";
}
Each += creates a new String and copies everything built so far, so total work is O(n²) in the output length. Building a 49,000-character result in 10,000 steps costs around 240 million character copies. Fix it with a single StringBuilder (pre-sized if you can estimate), or String.join(" ", words) / Collectors.joining.
Outside loops, + is fine: since Java 9 (JEP 280), a + b + c compiles to one invokedynamic call to StringConcatFactory, which computes the exact size and allocates once.
How I’d say it in the interview
"Strings are immutable, so each += copies the whole accumulated string: quadratic. In a loop I use a StringBuilder or String.join. A single expression with plus is fine; since Java 9 it compiles to an invokedynamic call that's as efficient as a builder."
Signals depth
Knowing the Java 9 change and that the old advice "always use StringBuilder" is outdated for single expressions.
Red flags
"The compiler converts it to a StringBuilder, so it's fine." It does a fresh concatenation per iteration either way.
Follow-ups interviewers ask next
What if multiple threads append to the same builder?
StringBuilderisn't thread-safe; concurrent appends can corrupt the content or throw.StringBufferis synchronized, but sharing a buffer across threads is usually a design smell. Build per thread and combine, or collect with a concurrent structure.
What's the difference between trim() and strip()?
Tests Java 11 API, Unicode whitespace
Show answerHide answer
Model answer
trim() (Java 1.0) removes leading and trailing characters with a code point up to U+0020: the space and ASCII control characters. strip() (Java 11) removes characters that Character.isWhitespace considers whitespace, which includes Unicode spaces like the em space U+2003. isBlank() (Java 11) is the matching "only whitespace" check. Prefer strip() in new code.
How I’d say it in the interview
"trim only knows about ASCII; strip is Unicode-aware. So for text pasted from a browser or Word, strip actually removes the odd spaces."
Signals depth
Knowing that neither removes the non-breaking space U+00A0, because Java doesn't classify it as whitespace.
Follow-ups interviewers ask next
What if the string is null?
Both throw
NullPointerException. If null is possible, check first or use a helper like Apache CommonsStringUtils.strip, which returns null for null.
Why is "😀".length() equal to 2?
Tests UTF-16, compact strings
Show answerHide answer
Model answer
length() returns the number of UTF-16 code units, and characters outside the Basic Multilingual Plane, like most emoji, take two (a surrogate pair). codePointCount(0, s.length()) or codePoints().count() returns 1.
Internally, since Java 9 (compact strings), a String stores a byte[] and a coder. Pure Latin-1 text uses one byte per char; anything else, like this emoji, switches the whole string to UTF-16, two bytes per char. length() still reports code units in both cases.
How I’d say it in the interview
"Java strings are UTF-16 at the API level, so an emoji is a surrogate pair of two chars. If I need what users see as characters, I count code points."
Signals depth
Mentioning compact strings and that a single non-Latin-1 character doubles the storage of the whole string. Bonus: grapheme clusters (a family emoji) can be several code points, which needs BreakIterator.
Red flags
"Because emoji are two bytes."
The user types 25, Enter, then Bob, Enter. Why is name empty?
Tests Scanner tokenization
Show answerHide answer
Model answer
Scanner sc = new Scanner(System.in);
int age = sc.nextInt();
String name = sc.nextLine(); // ""
nextInt() reads the token 25 and stops before the newline. nextLine() then returns the rest of the current line, which is empty. Fix it by reading lines and parsing (Integer.parseInt(sc.nextLine().strip())), or by calling sc.nextLine() once to consume the leftover newline.
How I’d say it in the interview
"nextInt leaves the line break in the buffer, so nextLine returns the empty rest of that line. I read whole lines and parse them, so there's never leftover state."
Signals depth
Knowing nextInt() throws InputMismatchException on bad input without consuming the token, so a retry loop must discard it with next().
Follow-ups interviewers ask next
What if the input has a million integers?
Scanneris regex-based and noticeably slow: roughly 400 ms for a million ints versus about 100 ms withBufferedReader+StringTokenizerin my measurements. For large input useBufferedReader, and batch output through aPrintWriterorStringBuilderinstead of a millionSystem.out.printlncalls.
A report shows prices as "19,99" for some customers. What happened?
Tests locale-sensitive formatting
Show answerHide answer
Model answer
The code formats numbers with String.format("%.2f", price) (or printf/formatted), which uses the JVM's default locale. On a server or user machine with a German or French locale, the decimal separator is a comma. If the output feeds a CSV, SQL or JSON, it breaks the parser. Fix: String.format(Locale.ROOT, "%.2f", price) for machine-readable output, and an explicit user locale (or NumberFormat) for display. For money, BigDecimal with explicit rounding is the right type in the first place.
How I’d say it in the interview
"Formatter follows the default locale, so a German JVM prints a comma. For anything a machine reads I pin Locale.ROOT; for humans I pass their locale explicitly."
Signals depth
Mentioning other locale traps: toLowerCase() in Turkish (dotless ı), Arabic locales producing non-ASCII digits with %d, and Scanner.nextDouble() rejecting "1.5" in a German locale.
Follow-ups interviewers ask next
What if you can't touch the code?
Set the default with
-Duser.language=en -Duser.country=US(or the format category properties) as a stopgap. It's a band-aid: the real fix is explicit locales at the call sites.
Design a fast, safe way to compare user-supplied status codes against a set of known values.
Tests equals idioms, switch on String, normalisation
Show answerHide answer
Model answer
Normalise once at the boundary, then compare with content equality:
enum Status { ACTIVE, SUSPENDED, CLOSED }
static Optional<Status> parse(String raw) {
if (raw == null || raw.isBlank()) return Optional.empty();
String key = raw.strip().toUpperCase(Locale.ROOT);
return switch (key) { // hashCode + equals under the hood
case "ACTIVE" -> Optional.of(Status.ACTIVE);
case "SUSPENDED" -> Optional.of(Status.SUSPENDED);
case "CLOSED" -> Optional.of(Status.CLOSED);
default -> Optional.empty();
};
}
Inside the system you pass the enum, so later comparisons are == on enum constants, which is correct because each constant is a singleton. switch on String compiles to a switch on hashCode() plus equals(), so it's O(1) on average and content-based.
How I’d say it in the interview
"I'd validate and normalise at the edge: null check, strip, uppercase with Locale.ROOT, then map to an enum. After that the rest of the code compares enums, where == is actually correct."
Signals depth
Converting to an enum instead of passing strings around, Locale.ROOT on case conversion, and knowing how switch on String compiles.
Red flags
Comparing with == "because they're constants", or toUpperCase() with no locale.
Follow-ups interviewers ask next
What if the input can be null and you want to keep the switch?
A classic
switchon anullString throwsNullPointerException. Check first, as above, or use Java 21's pattern-matching switch with an explicitcase null ->.