Home/Learn/Java A–Z/String Internals

String Internals

Intermediate
OOP Deep Dive

Strings are immutable, heap-allocated objects backed by a char/byte array. The string pool, interning, and compact strings have significant performance implications.

Overview

String is one of the most-used classes in Java, and its internals have evolved significantly. Java 9 introduced Compact Strings — storing Latin-1 strings as byte[] instead of char[], halving memory for ASCII-heavy workloads. The String pool (interned strings) ensures string literals share the same object. Understanding immutability, the pool, and performance characteristics is essential for optimising string-heavy code.

Immutability and the String Pool

String is immutable — once created, its value cannot change. This makes it inherently thread-safe and safe as HashMap keys. The JVM maintains a String pool (interned strings): string literals are automatically pooled; String.intern() manually adds strings to the pool.

== compares references; equals() compares content. String literals with == may coincidentally return true due to pooling, but never rely on this.

StringPool.java
// String literals are pooled automatically
String a = "hello";
String b = "hello";
System.out.println(a == b);       // true  — same pool object
System.out.println(a.equals(b));  // true  — same content

// new String() always creates a new heap object
String c = new String("hello");
System.out.println(a == c);       // false — different object
System.out.println(a.equals(c));  // true  — same content

// intern() moves heap string into pool
String d = c.intern();
System.out.println(a == d);       // true  — now same pool object

// String pool lives in Heap (since Java 7)
// Pre-Java 7 it was in PermGen — caused OOM for large apps

// Immutability means every "modification" creates a new String
String s = "hello";
s.concat(" world"); // returns new String, original unchanged
String result = s.concat(" world"); // must capture the return

Compact Strings (Java 9+)

Before Java 9, String stored characters as char[] (UTF-16, 2 bytes per char). Java 9 introduced Compact Strings: if all characters are Latin-1 (0-255), the string is stored as byte[] with 1 byte per char, halving memory for typical English text.

This is transparent to the developer but significant for memory footprint. The coder field (byte) tracks whether the string is LATIN1 (0) or UTF16 (1).

CompactStrings.java
// Java 9+ String internals (simplified)
public final class String {
    private final byte[] value;  // was char[] before Java 9
    private final byte coder;    // 0 = LATIN1, 1 = UTF16

    // charAt() adapts transparently
    public char charAt(int index) {
        if (coder == LATIN1) {
            return (char)(value[index] & 0xFF);
        } else {
            return StringUTF16.charAt(value, index);
        }
    }
}

// Memory comparison
// Java 8: "hello" = Object header (16) + char[5] (24) = ~40 bytes
// Java 9: "hello" = Object header (16) + byte[5] (21) = ~37 bytes (LATIN1)
// "Héllo" (1 non-Latin char) → still UTF16 → char[5] (26) = ~42 bytes

// Check via reflection (diagnostic only)
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
byte[] bytes = (byte[]) value.get("hello");
System.out.println(bytes.length); // 5 (LATIN1 — 1 byte each)

String Performance Patterns

String.charAt() is O(1). String.substring() creates a new object (O(n) copy since Java 7u6 — the shared backing array was removed to prevent memory leaks). String comparison: equals() is O(n); equalsIgnoreCase() is slower.

For repeated string construction, always use StringBuilder. For repeated pattern matching, compile Pattern once. String.format() is convenient but slow for hot paths — prefer String::formatted or direct concatenation.

StringPerf.java
// substring() — O(n) copy in modern Java (no memory leak risk)
String src = "Hello, World!";
String sub = src.substring(7, 12); // "World" — new String, new byte[]

// String comparison pitfalls
String s = "Hello";
s.equals("hello");           // false — case sensitive
s.equalsIgnoreCase("hello"); // true  — but slower
s.compareTo("Hello");        // 0 — lexicographic compare

// startsWith / endsWith — efficient
s.startsWith("He");    // true, O(prefix.length)
s.endsWith("lo");      // true, O(suffix.length)

// contains — uses indexOf internally
s.contains("ell");     // true, O(n*m) naive search

// String.valueOf vs toString
String fromInt = String.valueOf(42);    // null-safe: valueOf(null) = "null"
String fromInt2 = Integer.toString(42); // same
// obj.toString() throws NPE if obj is null
// String.valueOf(obj) returns "null" string safely

// Efficient number formatting — avoid String.format in hot paths
String fast = Integer.toString(n);    // much faster than String.format("%d", n)

Key Points to Remember

  • String is immutable — every "modification" creates a new object.
  • String literals are pooled; new String("x") creates a separate heap object.
  • Always use equals() for content comparison, never == (unless you know both are interned).
  • Java 9+ Compact Strings store Latin-1 text as byte[] — half the memory of char[].
  • substring() creates a new String (O(n) copy) — not O(1) as in some other languages.

Practice String Internals in the Playground

Run and modify code directly in your browser - no setup needed.

Interview Questions

Sign in to ask Aria
1

Why is String immutable in Java?

EasyAmazon
2

What is the String pool and where does it live in memory?

MediumGoogle
3

What is the difference between == and equals() for Strings?

EasyTCS
4

What are Compact Strings introduced in Java 9?

MediumOracle
5

Does substring() share the backing array with the original String in modern Java?

HardMicrosoft

Ask Aria about String Internals

Your personal AI tutor — ask anything about this concept