String Immutability - Java

  1. Which comparison result is guaranteed by String pooling and immutability?
    Answer: a == c is true because a is a pooled constant and intern() returns the canonical equal String. When working with Java String pooling, you need to distinguish between compile-time constants and runtime-computed Strings, then understand what intern() actually does. Java maintains a String pool (part of the heap) where it stores canonical String literals. When the compiler sees "ja" + "va", both operands are compile-time constants, so it folds them into "java" at compile time and places that value in the pool — meaning a holds a reference to the pooled "java". However, prefix + "va" involves a variable (prefix), so Java cannot resolve it at compile time; instead, it creates a new String object on the heap at runtime, which b references. That object has the same characters but lives outside the pool. This is why B is correct: c = b.intern() looks up the pool for a String equal to b's content ("java"), finds the existing pooled constant (the one a already references), and returns that same reference. So a == c compares two references pointing to the same pooled object — guaranteed true. A is wrong because only fully constant expressions are compile-time folded. Since b involves a variable, it is computed at runtime and not automatically pooled. C is wrong because intern() does not modify b itself — it returns a reference to the pool entry, leaving b unchanged as a separate heap object, so b == c is false. D is wrong because .equals() always compares character content, not identity — a.equals(c) is absolutely true. Remember: == tests reference identity; intern() returns a pooled reference but never changes the original variable.
  2. What is printed?
    Answer: [ a b ], because trim() and replace() results are discarded, while only toLowerCase() is assigned back. Whenever you see a question about String manipulation in Java, the first thing to remember is that Strings are immutable — once created, a String object cannot be changed. Every method like trim(), toLowerCase(), and replace() returns a new String rather than modifying the original. This means if you don't capture the return value, that transformation is simply lost. Walking through normalize(" A B ") step by step confirms D is correct. First, value.trim() is called but its return value is thrown away — value still holds " A B ". Second, value = value.toLowerCase() is the only line that reassigns value, so now value is " a b " (spaces preserved). Third, value.replace(" ", "_") produces a new String "_a_b_", but again the result is discarded. So the method returns " a b ", and the output is [ a b ]. As for the wrong answers: A reflects the most common misconception — that String methods mutate the object in place, like methods on a StringBuilder might. They don't. B is tricky because it implies replace does affect the result even without assignment, which is impossible given immutability. C makes the false claim that trim() modifies the original object (it doesn't — its result is discarded just like the others). A reliable study tip: in Java, any time you call a String method, ask yourself "is this return value being stored somewhere?" If not, the transformation might as well not exist. When reviewing code, trace each assignment (=) carefully — that's your real map of what actually changes.
  3. Which statement most accurately describes the concurrency implications?
    Answer: Individual String objects cannot be corrupted by readers, but the read-concatenate-write update is not made atomic by immutability. When you see a concurrency question involving String in Java, separate two distinct concerns: the safety of individual String objects versus the safety of the operation that updates a shared reference. String objects in Java are immutable — once created, their internal character data never changes. This means a reader thread can safely dereference a String object and read its contents without fear of another thread corrupting the characters mid-read. That part is thread-safe by design. However, immutability says nothing about the shared variable (status) that holds the reference. The expression status = status + "!" is actually three steps: read status, create a new String by concatenation, then write the new reference back. Without synchronization, two threads can both read the old value, both create their own new String, and then both write back — causing one update to be silently lost. This is a classic read-modify-write race condition, and C captures this precisely. A is wrong because String immutability prevents mutation of pooled literals — no thread can alter "READY" in the string pool. The exclamation marks go into a brand-new object, not the original. B is tempting but dangerously false: immutability makes the String object safe, but it does not make the multi-step assignment atomic. Atomicity requires explicit mechanisms like synchronized or AtomicReference. D is backwards — readers don't need synchronization because of internal character arrays; String immutability already guarantees those can't change. The key study tip: always ask two separate questions — "Is the object safe?" (yes, for Strings) and "Is the operation on the shared variable atomic?" (no, without explicit synchronization).
  4. Why does this class generally not need defensive copies of region in its constructor or getter?
    Answer: Because String is immutable, callers can reassign their references but cannot alter the stored object's character content. When a question asks about defensive copies, you should immediately think about mutability — specifically, whether the object being stored or returned can be changed by an outside caller after the fact. In Java, String is immutable by design. Once a String object is created, its character content can never be changed. A caller can reassign their own reference variable to point elsewhere, but that has no effect on the String object stored inside ServiceConfig.region. Because no code path can alter the content of the stored String, there's nothing to defend against — returning the reference directly is perfectly safe. That's why C is correct. A contains a subtle but important error: final on a field means the reference cannot be reassigned after construction, but it says nothing about the object pointed to. If region were a StringBuilder instead, marking the field final would still allow callers to mutate the content through the returned reference. Immutability of the object itself is what matters, not finality of the field. B is simply false. Java does not automatically clone String objects on assignment or return — no such language feature exists. You'd have to write that defensive copy code yourself. D is also false. Returning a String does not automatically intern it. Interning (via String.intern()) is an explicit operation that places strings in a shared pool; it has nothing to do with protecting field references from external mutation. As a study rule: **final protects the reference; immutability protects the data.** For defensive copies, always ask what can mutate — not what is final.
  5. Why are String keys generally safer for this use, assuming the textual value is not supposed to change after insertion?
    Answer: A String's content cannot change after insertion, so its equality and hash behavior remain stable for later lookups. When a question asks about HashMap key safety, think about what the map actually relies on to find values: a key's hashCode() and equals() must remain consistent throughout the key's lifetime in the map. If either changes after insertion, the map can no longer locate the entry — it looks in the wrong bucket and returns null, even though the data is still there. This is exactly why D is correct. String in Java is immutable — once created, its characters cannot change. That means its hashCode() and equals() behavior are permanently stable. You can insert a String key, retrieve it a million times later, and the hash will always point to the same bucket. The wrong answers each describe behaviors that don't actually exist. A is false: HashMap does not recompute hashes after every insertion — it caches the hash (in fact, String caches its own hash) and never updates bucket locations dynamically. B is false: HashMap uses equals() for key comparison, not reference equality — that's what == does, and confusing the two is a classic trap. C is false: Java only interns String literals automatically, not every String inserted into a map; explicitly calling intern() is required otherwise, and even then, interning is about identity, not hash stability. A useful rule of thumb: whenever you see a question about HashMap correctness, immediately ask yourself "could the key's hash or equality change?" Immutability is the safeguard — mutable keys are a well-known Java pitfall worth memorizing for your exam.
  6. Which design choice follows most directly from the implications of String immutability?
    Answer: Use a mutable char[], then overwrite its elements after validation because a String's contents cannot be erased in place. When you see a question about sensitive data and Strings in Java, the key concept to anchor on is immutability: once a String object is created, its internal character array cannot be modified. The JVM controls when that memory is garbage-collected, and there's no way to force the sensitive bytes to be zeroed out early — they linger in the heap until the GC decides to reclaim them. This is exactly why B is correct. A char[] is mutable, meaning you can explicitly overwrite each element (e.g., Arrays.fill(password, '\0')) the moment validation is complete. You control the erasure timing. This is precisely why javax.crypto.spec.SecretKeySpec and java.io.Console.readPassword() both work with char[] rather than String. A is wrong because String.replace() doesn't overwrite the original object's internal storage — it creates a new String object. The original, with the sensitive characters, still exists in memory until GC runs. C contains a dangerous myth: setting a reference to null does not immediately clear the object or its memory, and interned Strings in the string pool are held even longer by the JVM, making them harder to collect, not easier. D is wrong because StringBuilder.toString() produces a new, immutable String — the resulting object has the same uncontrollable lifetime problem as any other String, so you haven't gained anything over using a String directly. A good study tip: whenever a Java question involves security-sensitive data, remember the rule — **String for display, char[] for secrets**. This pattern appears in real APIs and on exams alike.
  7. Which statement correctly distinguishes the effects of final and String immutability in this code?
    Answer: It prints A; concat is legal but returns an unused object, while final prevents reassigning label. When Java code involves final variables and Strings, you need to track two separate concepts: what final does to a reference, and what immutability does to an object. In Java, final applied to a variable means the reference cannot be reassigned — label will always point to the same String object. It says nothing about whether that object can be mutated. Separately, String immutability means no String object's characters can ever change after creation. These are independent guarantees that work together. In the passage, label.concat("B") is perfectly legal to call — it doesn't reassign label and doesn't violate immutability. What it does is create a brand-new String "AB" and return it. Since the return value is discarded (not stored anywhere), label still points to "A". So System.out.println(label) prints A. That makes A correct: concat creates an unused object, and final is simply preventing reassignment. B is wrong because Strings are immutable — their characters cannot change, period. No reference modifier changes that. C is wrong because calling a method on a final reference is completely legal; final only blocks statements like label = "AB". D is wrong on two counts: final does not make the object immutable (that's String's own property), and there is no such thing as "in-place concatenation" on a String regardless of how the reference is declared. A useful mental model: think of final as locking the pointer, not the object. String immutability locks the object itself. On exam questions like this, always ask both questions separately.
  8. What is printed, and what property is demonstrated?
    Answer: A|AB; each toString() result is an immutable snapshot unaffected by later builder changes. Whenever you see a question mixing StringBuilder with String, ask yourself: what happens to a String once it's created? That's the core concept here — String immutability. When you call builder.toString(), Java creates a brand-new String object that copies the builder's current character sequence. That String is then completely independent — it has its own memory, and nothing can change it afterward. So first captures "A" at that moment, and no matter what happens to builder later, first stays "A". When builder.append("B") runs and you call toString() again, second captures "AB". Printing first + "|" + second therefore gives you A|AB, making A the correct answer — each toString() call produces an immutable snapshot frozen at the time of the call. B is wrong because String objects do not share the internal storage of the StringBuilder. The builder and the resulting strings are entirely separate objects after toString() returns. C describes a fictional behavior — calling toString() has absolutely no effect on the builder's mutability; you can keep appending freely afterward. D inverts the actual output and invents a nonexistent "caching" mechanism; the builder never reaches back to modify previously created String objects. A useful rule of thumb: in Java, once a String exists, it is set in stone. StringBuilder is the mutable workspace; String is the finished, unchangeable product. On exam questions, always trace when each toString() is called relative to any mutations.
  9. Assume words contains many short elements. Which statement best explains why this code can perform poorly as the collection grows?
    Answer: Each iteration can create a new result and copy accumulated characters; a StringBuilder can reduce that repeated work. Whenever you see a question about String concatenation in a loop, think about immutability — the core property that drives Java String performance. Because Strings in Java cannot be changed after creation, the + operator doesn't modify an existing String; it creates a brand new one each time. Here's what actually happens in that loop: on each iteration, Java allocates a new String object, copies all the characters accumulated so far into it, then appends the next word. If words has n elements with average length k, the total characters copied grows as k+2k+3k+⋯+nk=k⋅n(n+1)2k + 2k + 3k + \cdots + nk = k \cdot \frac{n(n+1)}{2}, giving O(n²) behavior. A StringBuilder instead maintains a resizable internal buffer, so appending is typically O(1) amortized — making the full loop O(n). That's exactly what A describes, making it correct. B is wrong because Java Strings are immutable — there is no mutation of an existing String, ever. Length recalculation isn't the issue here either. C is wrong because string concatenation via + at runtime does not intern intermediate results in the String pool. Interning is a separate, explicit mechanism (or applies to compile-time constants), not automatic loop concatenation. D is wrong because the compiler does not guarantee a single allocation for runtime concatenation in a loop. While modern compilers optimize single-statement concatenations, they cannot collapse a loop into one allocation. Reference reassignment itself isn't the bottleneck. As a study tip: whenever you see String concatenation inside a loop, immediately think StringBuilder — it's one of the most common Java performance patterns tested on exams.
  10. What is printed, and which explanation best accounts for the result?
    Answer: core|Core, because concat is ignored and replace returns a reassigned String. Whenever you see a question involving String operations in Java, the most important concept to keep in mind is immutability: String objects cannot be changed after creation. Every method that appears to "modify" a String actually returns a new String object — it never alters the original. Walking through the code confirms why B is correct. After String second = first;, both variables point to the same "core" object. Then first.concat("-api") is called — but its return value is discarded because it's never assigned back to first. The original "core" object is untouched, and first still points to it. Next, first = first.replace('c', 'C') creates a new String "Core" and reassigns first to it. Meanwhile, second was never reassigned, so it still holds "core". The output is core|Core, exactly what B describes. A is wrong because second and first stop being aliases the moment first is reassigned by replace. Reassignment changes which object a variable points to, not the object itself. C is wrong because it assumes concat mutated the shared object — it didn't; the result was thrown away. D is wrong because it claims replace also fails, but replace does work — it just requires you to capture the returned value, which the code correctly does with first = first.replace(...). As a study tip: always check whether a String method's return value is assigned. If not, the operation has no lasting effect. This is one of the most common traps on Java exams involving String manipulation.

Start this quiz