Back to String & String Pool
Q9Question 9 of 12

Why are strings immutable in Java?

Key Concepts
  • See Q19 for the comprehensive answer on String immutability
  • This question covers the same concept with additional emphasis on hashCode caching and class loading security
  • String's internal char/byte array is private final and never exposed via any method
  • Every String method that appears to modify returns a NEW String – substring, replace, toUpperCase, trim
  • Compact Strings (Java 9+) store LATIN1 as byte[] (1 byte/char), halving memory for ASCII strings

Definition

String is immutable because the class is final, the internal array is private final, and no method modifies it. Every modification method returns a new String.

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+)

JAVA
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

JAVA
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.

JAVA
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:

JAVA
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:

JAVA
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.

JAVA
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
    }
}
TEXT
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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate