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:
- 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 distinctStringinstance on the Regular Heap.
- Object 1: The string literal
- Warm State (Literal already present in String Pool):
Exactly 1 object is created:- Only the new
Stringinstance on the Regular Heap is created; the literal"Hello"is reused directly from the String Pool.
- Only the new
🔴 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
Stringwrapper on the Regular Heap simply references the exact same internalbyte[] valuearray owned by the pooled literal! - The waste of memory is strictly the redundant
java.lang.Stringobject wrapper:- 12 bytes: Object Header (Mark Word + Compressed Klass Pointer).
- 4 bytes: Compressed reference pointer to
valuearray. - 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 newStringwrapper pointing to the original largechar[]array, merely configuring differentoffsetandcountoffsets. - 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 thenewoperator. - 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:
- Byte Decoding in I/O:
new String(byteArray, StandardCharsets.UTF_8)when converting network packets or file bytes into characters with an explicit charset. - Character Buffer Conversion:
new String(charArray)when extracting characters built in custom mutable buffers. - Subarray Decoding:
new String(bytes, offset, length, charset).
Callingnew 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 theldcbytecode opcode with zero runtime heap allocation. Callingnew 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 underlyingbyte[] valuearray, 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
- Literal Path: Resolves via String Pool, single
ldcbytecode instruction, zero heap allocation on cache hit. - Constructor Path: Allocates on Regular Heap,
s1 != s2, buts1.equals(s2) == true. - Object Count: 1 (if literal was already pooled) or 2 (literal in pool + new object on heap).
- Underlying Array Sharing: In OpenJDK,
new String(String)sharesthis.value = original.value;. - Constant Folding:
javacfolds concatenations of literals andfinalconstants 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:Stringis already immutable and inherently thread-safe).
Related Topics
- How String Pool Works — Pool internals and GC
- When to Use intern() — Manual pool registration
- Can You Use == to Compare Strings — Reference vs content comparison
- How Java Compiler Optimizes String Concatenation — Constant folding & invokedynamic