skip to content

As an API designer, how do you reason about the trade-offs of String immutability, and when does it become a liability (e.g. sensitive data)?

level: principalimportance: nice to knowfreq 34%

answer

  1. Immutable = safe default, but cannot be erased
  2. Secret in String lingers: heap, swap, dump, pool
  3. char[] is mutable -> overwrite in finally
  4. JDK uses char[] for passwords (KeyStore, getPassword)
  5. Erasure must be end-to-end; any String touch reopens exposure

basics

~20 s

Immutability makes Strings safe and easy to share, but it has a downside: you cannot wipe a String from memory after use. For passwords and secrets, prefer char[] (or a byte buffer) that you can overwrite, because immutable Strings linger in memory and the pool.

solid answer

~50 s

Immutability is mostly a win — it gives safe sharing, free thread-safety, stable hashing, and protection against time-of-check/time-of-use tampering, which is why String is the canonical value type for paths, names, and identifiers. The principal-level liability is that you can't deterministically clear a String: its bytes stay on the heap (and possibly the intern pool) until GC, and even then memory isn't zeroed, so a secret can be recovered from a heap dump. That's why JDK APIs accept char[] for passwords (e.g. JPasswordField.getPassword, KeyStore) — a char[] is mutable and can be overwritten right after use to shrink the exposure window. As a designer you weigh this: use String for ordinary values, but for high-value secrets use a clearable mutable carrier and zero it in a finally block. The broader lesson is that immutability is a default-good design choice (defensive-copy-free APIs, value semantics) whose one notable failure mode is secure erasure.

code

java · 10 lines
java
// Secret in a clearable char[] - can be zeroed; a String cannot
char[] password = console.readPassword();
try {
    boolean ok = authenticate(username, password);
    // ...
} finally {
    java.util.Arrays.fill(password, '\0'); // shrink the exposure window
}
// With a String you could not do this - it lingers until GC, never zeroed,
// and may persist in the intern pool / heap dumps.

go deeper

for a junior

Knows passwords should be char[] not String, even if not why.

for a middle

Explains that a String can't be erased and lingers, so char[] is preferred for secrets.

for a senior

Details the heap/pool/dump lifetime, the finally-zeroing idiom, and which JDK APIs use char[].

for a principal

Frames immutability as a default design contract with one narrow liability (secure erasure), reasons about end-to-end secret lifetime and when a mutable carrier is the right exception without discarding immutability's benefits elsewhere.

## Recap: why immutability is the right default An **immutable** object can't change after construction. For a value type like `String` this yields: safe sharing without defensive copies, inherent thread-safety, a cacheable/stable hash (great map keys), and resistance to **TOCTOU** (time-of-check/time-of-use) attacks where a value is validated then swapped before use. These are exactly the properties you want for identifiers, configuration, paths, and class names. As an API designer, *prefer immutable value types by default* — they make code easier to reason about and APIs safe to expose. ## The liability: you cannot erase a String The key downside is **non-erasability**. Once a secret lives in a `String`: 1. You can't overwrite its contents — it's immutable, so there's no `clear()`. 2. The object stays on the heap until the garbage collector reclaims it, which is non-deterministic (you don't control *when*). 3. Even after GC, the memory is typically **not zeroed**; the bytes linger until reused. 4. If the literal was interned or came from a literal, it may live in the **string pool for the JVM's lifetime**. Consequence: a password held as a `String` can show up in a **heap dump**, a swap file, or a core dump long after you 'finished' with it. There's no way to shrink that exposure window. ## The standard remedy: char[] (or off-heap buffers) This is why security-sensitive JDK APIs use `char[]` instead of `String`: - `javax.swing.JPasswordField.getPassword()` returns `char[]`. - `java.security.KeyStore` and `PBEKeySpec` take `char[]`. A `char[]` is **mutable**, so you can overwrite it as soon as you're done, drastically shrinking the time the secret is recoverable: ```java char[] pw = readPassword(); try { authenticate(pw); } finally { java.util.Arrays.fill(pw, '\0'); // zero it out; not possible with String } ``` (Even better for some threat models: a direct `ByteBuffer` or OS-backed memory you can lock/clear, but `char[] + finally` is the standard JDK idiom.) ## How a principal frames the trade-off - **Default to immutability** for value types — it's a force multiplier for correctness, concurrency, and API safety, and removes the need for defensive copies. - **Recognize the one failure mode**: deterministic secure erasure. For passwords, key material, and tokens, choose a *mutable, clearable* carrier and zero it in `finally`. - **Don't over-rotate**: the erasure concern is real but narrow. It applies to genuine secrets, not to ordinary strings; turning everything into `char[]` would throw away immutability's benefits for no gain. - **Consider the whole pipeline**: a secret read into a `char[]` is undone if you concatenate it into a log line, an exception message, or a `String` for an HTTP header — the moment it touches a `String`, the exposure returns. Erasure discipline must be end-to-end. The meta-point: immutability is a deliberate design contract with well-understood benefits and exactly one classic liability. A principal engineer applies it as the default and knows the specific case (secure data lifetime) where a mutable type is the correct exception.

  • Why can't you securely clear a password stored in a String?
    String is immutable, so there is no method to overwrite its contents. The object stays on the heap until non-deterministic GC, the memory isn't zeroed, and it may persist in the intern pool, so a heap dump can still reveal it.
  • Does using char[] fully solve secret exposure?
    No. It shrinks the window by letting you zero it after use, but the discipline must be end-to-end: converting the secret to a String for logging, an exception, or an HTTP header reintroduces the lingering, non-erasable copy.

saying these in an interview costs you the question

  • Storing passwords/secrets in String long-term
  • Believing GC promptly and securely wipes a String
  • Turning every value into char[] and losing immutability's benefits
  • Zeroing a char[] but then logging the secret as a String anyway

context