Definition
String immutability guarantees:
- final class: Cannot be subclassed to add mutable behavior.
- private final byte[] value: The internal storage is never exposed.
- No mutating methods: replace(), toUpperCase(), substring() all return NEW Strings.
- hashCode caching: Computed once, stored in a private field, never invalidated.
A bank's tamper-proof receipt. The paper is sealed (final), the ink is permanent (private final byte[]), and any correction produces a new receipt (new String) instead of erasing the original.
Understanding the Concepts
Internal structure (Java 9+)
public final class String implements Serializable, Comparable<String>, CharSequence {
private final byte[] value; // compact: LATIN1 (1 byte) or UTF16 (2 bytes)
private final byte coder; // 0 = LATIN1, 1 = UTF16
private int hash; // cached hashCode, default 0
private boolean hashIsZero; // distinguishes "not computed" from "hash is 0"
}
Every "modification" creates a new String
String original = "National Bank of India";
String upper = original.toUpperCase(); // new String
String replaced = original.replace("India", "Indonesia"); // new String
String trimmed = " hello ".trim(); // new String
String sub = original.substring(0, 8); // new String (Java 7u6+)
System.out.println(original); // "National Bank of India" (unchanged)
Compact Strings (Java 9+)
Strings containing only ASCII/Latin1 characters use 1 byte per character instead of 2. This halves memory for most English text.
String ascii = "SAV2001"; // stored as LATIN1 → 7 bytes (not 14)
String hindi = "खाता"; // stored as UTF16 → 8 bytes (4 chars × 2)
What should a senior engineer know?
- substring() no longer shares the underlying array since Java 7u6. Each substring creates an independent copy to prevent memory leaks.
- Reflection can break immutability:
Field f = String.class.getDeclaredField("value"); f.setAccessible(true); f.set(str, newBytes);– but Java modules block this by default. - GraalVM native-image stores String pool entries in the image heap, making startup faster.
What are common mistakes?
A frequent mistake is using == to compare String content instead of equals().
Wrong approach:
package com.bank.service;
public class AccountValidator {
public boolean validateAccountType(String type) {
// WRONG: == compares references, not content
if (type == "SAVINGS") {
return true;
}
return false;
}
}
Correct approach:
package com.bank.service;
public class AccountValidator {
public boolean validateAccountType(String type) {
// CORRECT: equals() compares content
if ("SAVINGS".equals(type)) { // null-safe: literal first
return true;
}
return false;
}
}
Using the literal on the left side of equals() prevents NullPointerException when the variable is null. This is a standard practice at National Bank of India for all string comparisons in service code.
What are the edge cases?
String behavior has several surprising edge cases that can catch banking developers off guard.
package com.bank.edge;
public class StringEdgeCases {
public static void main(String[] args) {
// Edge case 1: compile-time constant folding
String a = "SAV" + "2001"; // Compile-time constant
String b = "SAV2001";
System.out.println(a == b); // true (same pool entry)
// Edge case 2: variable concatenation is NOT pooled
String prefix = "SAV";
String c = prefix + "2001"; // Runtime concatenation
System.out.println(b == c); // false (different objects)
// Edge case 3: final variable IS a compile-time constant
final String finalPrefix = "SAV";
String d = finalPrefix + "2001"; // Compile-time constant
System.out.println(b == d); // true (same pool entry)
// Edge case 4: substring no longer shares char array (Java 7u6+)
String full = "AccountNumber:SAV2001";
String sub = full.substring(14); // New String, new char array
}
}
true
false
true
The final keyword makes the variable a compile-time constant, which changes how the compiler handles concatenation.
What are common interview follow-up questions?
Interviewers often follow up with these related questions:
- How does this concept apply to a high-throughput banking transaction system?
- What are the performance implications in a production environment?
- How would you debug issues related to this concept?
- What tools would you use to monitor this in production?
- How does this behave differently in Java 8 vs Java 17+?
When answering follow-up questions, always relate your answer back to practical experience. For example, at National Bank of India, Rajesh debugged a production issue related to this concept by using JFR recordings and heap dumps. Providing concrete examples from banking systems shows the interviewer that you understand both the theory and the practice.
Visual Overview
String internals:
final class String → cannot subclass
private final byte[] value → cannot access or modify
private int hash → cached on first hashCode()
Every method returns a NEW String:
toUpperCase() → new String
replace() → new String
substring() → new String
trim() → new String
Compact Strings (Java 9+):
ASCII → LATIN1 encoding → 1 byte/char → half memory
Non-ASCII → UTF16 encoding → 2 bytes/char
Key Takeaway
String is immutable because the class is final, the internal byte array is private and final, and every method that appears to modify creates a new String instead. HashCode is cached after first computation. Compact Strings (Java 9+) halve memory for ASCII content.
This question overlaps with Q19. See Q19 for the comprehensive explanation of security, String Pool, thread safety, and HashMap key stability.
One sentence to remember: String is final, its internal array is private final, every modification returns a new object, and hashCode is cached.
---