🔤 Section 12 · Question #2

Difference Between Creating String via Literal and via new

In Java, a String instance can be created through two fundamentally different mechanisms: via a string literal or via an explicit constructor call with new String(...).


🟢 Junior Level

In Java, a String instance can be created through two fundamentally different mechanisms: via a string literal or via an explicit constructor call with new String(...).

The Essence in 30 Seconds

  • String Literal (String s = "Java";):
    The JVM checks the String Pool in the Java Heap. If "Java" was already allocated, the JVM creates zero new objects; it reuses the existing canonical instance and returns its reference.
  • Constructor (String s = new String("Java");):
    The JVM always allocates a brand new object in regular Heap memory (outside the String Pool), even if an identical string already exists in the pool.
// String literals: reuse pooled canonical instances
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true (point to the identical heap address)

// The 'new' operator: always allocates a separate object
String s3 = new String("Hello");
System.out.println(s1 == s3); // false (different objects in memory!)

// Content comparison via equals() always checks character values:
System.out.println(s1.equals(s3)); // true

Comparative Summary

Feature String Literal ("text") Constructor (new String("text"))
Object Location String Pool (inside Java Heap) Regular Heap (outside pool)
Deduplication / Reuse Yes (automatic runtime deduplication) No (every new allocates a new instance)
Memory Consumption Minimal (0 bytes if already present in pool) Wastes object header and reference memory
Execution Performance Instantaneous (single ldc bytecode opcode) Slower (heap allocation + <init> execution)
Recommendation 100% of standard application code Obsolete anti-pattern for string literals

🟡 Middle Level

Bytecode Analysis via javap -c

Examining the compiled bytecode instructions highlights why the literal approach is vastly more efficient:

1. Creation via Literal:

String s = "Hello";

Bytecode:

0: ldc           #2  // String Hello (loads reference from Constant Pool)
2: astore_1          // stores reference into local variable s

Execution requires a single ldc instruction! The JVM simply retrieves the pre-resolved object reference from the constant pool. No heap allocation occurs during method execution.

2. Creation via new String(...):

String s = new String("Hello");

Bytecode:

0: new           #3  // class java/lang/String (allocates raw memory on Heap)
3: dup               // duplicates reference on top of operand stack
4: ldc           #2  // String Hello (loads literal from String Pool!)
6: invokespecial #4  // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1          // stores reference to new instance into local variable s

The JVM executes four resource-intensive operations: raw heap allocation (new), operand stack duplication (dup), literal lookup (ldc), and constructor execution (invokespecial).

The Classic Interview Question: “How Many Objects Are Created?”

A staple technical interview question is:

“How many objects are created by String s = new String("Hello");?”

The precise engineering answer depends on the JVM’s prior state:

  1. Cold State (Literal not previously loaded):
    2 objects are created:
    • Object 1: The string literal "Hello" is resolved and registered into the String Pool during class loading / constant pool resolution.
    • Object 2: The new String(...) expression allocates a distinct String instance on the Regular Heap.
  2. Warm State (Literal already present in String Pool):
    Exactly 1 object is created:
    • Only the new String instance on the Regular Heap is created; the literal "Hello" is reused directly from the String Pool.

🔴 Senior Level

HotSpot Internals: The String(String original) Constructor

Looking at the OpenJDK source code (Java 9+ with Compact Strings), the copy constructor is implemented as follows:

// java.lang.String (OpenJDK 17/21)
public String(String original) {
    this.value = original.value; // NOTICE: The backing byte[] array IS SHARED!
    this.coder = original.coder;
    this.hash = original.hash;
}

Senior Engineering Insight:
Calling new String("Hello") does NOT allocate a second character array in memory:

  • The new String wrapper on the Regular Heap simply references the exact same internal byte[] value array owned by the pooled literal!
  • The waste of memory is strictly the redundant java.lang.String object wrapper:
    • 12 bytes: Object Header (Mark Word + Compressed Klass Pointer).
    • 4 bytes: Compressed reference pointer to value array.
    • 4 bytes: Cached int hash.
    • 1 byte: byte coder (LATIN1 vs UTF16).
    • 1 byte: boolean hashIsZero.
    • 2 bytes: 8-byte alignment padding.
    • Total: Exactly 24 wasted bytes in the heap per redundant new String() call.

Constant Folding vs. Runtime Concatenation

Understanding the boundary between compile-time constant optimization and runtime evaluation is vital:

// Example 1: Constant Folding (JLS §15.28)
String s1 = "hello world";
String s2 = "hello" + " " + "world"; 
// The javac compiler folds compile-time literal expressions into a single string!
// In bytecode, s2 compiles directly to `ldc "hello world"`.
System.out.println(s1 == s2); // true (points to identical pooled instance)

// Example 2: Compile-Time Constants via 'final'
final String a = "hello";
final String b = " world";
String s3 = a + b; 
// Because 'a' and 'b' are final variables initialized with constant literals,
// javac evaluates 'a + b' at compile time and folds it into "hello world"!
System.out.println(s1 == s3); // true!

// Example 3: Non-final Variables (Runtime Concatenation)
String c = "hello";
String d = " world";
String s4 = c + d; 
// In Java 9+, javac emits an `invokedynamic` instruction (StringConcatFactory).
// A BRAND NEW String instance is created on the Regular Heap (not pooled).
System.out.println(s1 == s4); // false!
System.out.println(s1 == s4.intern()); // true (intern returns the pooled reference)

Historical Context: Why new String(String) Was Invented

Prior to Java 7 Update 6 (2012), java.lang.String was implemented with four fields:

// Java 6 String layout:
char[] value;
int offset;
int count;
int hash;
  • In Java 6, calling substring() did not copy characters. It created a new String wrapper pointing to the original large char[] array, merely configuring different offset and count offsets.
  • The Memory Leak Hazard: Reading a 100 MB XML file and taking String id = largeText.substring(0, 5) retained the entire 100 MB character array in memory!
  • The Java 6 Workaround: Developers used new String(largeText.substring(0, 5)). The copy constructor forced a defensive copy of only the 5 required characters (Arrays.copyOfRange), allowing the 100 MB array to be garbage collected.
  • Modern Java (7u6+): substring() always copies character arrays. Consequently, new String(String) has zero valid modern use cases.

4 Tricky Interview Questions

1. How many objects are created by String s = new String("Java");, and why can the answer be either 1 or 2?

Answer:

  • 2 objects are created if the string literal "Java" has not been previously referenced in the JVM. The first object is the string literal registered in the String Pool during class constant pool resolution. The second object is the new instance allocated on the Regular Heap by the new operator.
  • Exactly 1 object is created if the literal "Java" was already loaded into the String Pool by earlier code or class loading. In that case, only the Regular Heap instance is allocated.

2. What is the evaluation of s1 == s2 in the following code snippet, and why:

final String a = "foo";
final String b = "bar";
String s1 = "foobar";
String s2 = a + b;

Answer:
It evaluates to true.
Under the Java Language Specification (JLS §15.28 Constant Expressions), because a and b are final variables of type String initialized with compile-time constant expressions, the concatenation a + b is treated as a compile-time constant.
The javac compiler folds the expression into the literal "foobar" at compile time and emits ldc "foobar". At runtime, s2 references the exact same canonical instance in the String Pool as s1. If either variable lacks the final modifier, concatenation occurs at runtime via invokedynamic, producing a distinct heap object where s1 == s2 is false.

3. Do s1 and s2 share the exact same underlying byte[] array in Java 21:

String s1 = "Hello";
String s2 = new String(s1);

Answer:
Yes, they share the identical byte[] array.
In the OpenJDK source code for public String(String original), the internal array is assigned directly: this.value = original.value;.
Because String and its internal byte array are immutable and encapsulated, the JVM avoids copying the raw bytes. Consequently, while s1 and s2 are distinct object references (s1 != s2), their internal value pointers reference the exact same backing array on the Heap.

4. What are the only legitimate use cases for calling new String(...) in modern Java?

Answer:
The new String(...) constructor is valid only when decoding raw binary streams or character buffers:

  1. Byte Decoding in I/O: new String(byteArray, StandardCharsets.UTF_8) when converting network packets or file bytes into characters with an explicit charset.
  2. Character Buffer Conversion: new String(charArray) when extracting characters built in custom mutable buffers.
  3. Subarray Decoding: new String(bytes, offset, length, charset).
    Calling new String(String) with an existing string literal is considered an anti-pattern with no valid use case in modern Java.

🎯 Interview Cheat Sheet

30-Second Summary

“Creating a string via a literal ("Java") leverages the String Pool: the JVM reuses existing canonical instances via the ldc bytecode opcode with zero runtime heap allocation. Calling new String("Java") forces the creation of a redundant object in Regular Heap, bypassing the pool. Under the hood in modern OpenJDK, new String(str) shares the same underlying byte[] value array, wasting 24 bytes on the object header alone. In response to ‘How many objects are created by new String(“text”)?’, the answer is two if the literal was not previously pooled, and one if it was already loaded.”

Key Concepts Checklist

  1. Literal Path: Resolves via String Pool, single ldc bytecode instruction, zero heap allocation on cache hit.
  2. Constructor Path: Allocates on Regular Heap, s1 != s2, but s1.equals(s2) == true.
  3. Object Count: 1 (if literal was already pooled) or 2 (literal in pool + new object on heap).
  4. Underlying Array Sharing: In OpenJDK, new String(String) shares this.value = original.value;.
  5. Constant Folding: javac folds concatenations of literals and final constants into single pooled strings at compile time.

Common Pitfalls & Red Flags

  • ❌ “Calling new String("Hello") always creates exactly one object.” (Fails to mention literal pool initialization).
  • ❌ “The new String(String) constructor creates a deep copy of the character array.” (False: in OpenJDK, the backing array reference is shared).
  • ❌ “We should use new String(str) to protect strings from mutation in multithreaded code.” (False: String is already immutable and inherently thread-safe).