2.3 The String Class, Immutability, and the String Constant Pool
Key Takeaways
- String is an immutable reference type; once instantiated in heap memory, its encapsulated character sequence can never be altered, lengthened, or shortened.
- Every String transformation method (such as toUpperCase(), trim(), or substring()) produces and returns a brand-new String object, leaving the original String instance unmodified.
- The String Constant Pool is a specialized memory region inside the heap that caches string literals, allowing identical literal values to share the same object reference.
- Instantiating strings using the new keyword (e.g., new String("text")) explicitly bypasses the pool, allocating a separate object on the general heap.
- The == operator compares memory reference addresses, whereas the equals() method compares actual character sequences; always use equals() for logical content comparison.
2.3 The String Class, Immutability, and the String Constant Pool
Quick Summary: In Java, strings are represented by the
java.lang.Stringclass. Unlike primitives, strings are reference objects allocated on the heap. Strings possess the unique property of immutability—once created, their internal character contents can never be changed. Any method that appears to alter a string actually instantiates and returns a brand-newStringobject. To optimize memory consumption, Java maintains the String Constant Pool, reusing identical literal values. Developers must compare strings using.equals()rather than==.
1. The Nature of the String Class
The java.lang.String class is one of the most foundational classes in the Java standard library. Because text manipulation is ubiquitous, the Java compiler grants String special syntax privileges not shared by regular reference types, including double-quoted literals ("Hello") and the overloaded concatenation operator (+).
However, it is vital for Exam 1Z0-811 to remember that String is not a primitive type:
Stringvariables store references (memory pointers) to objects on the heap.- A
Stringreference can hold the valuenull. Stringobjects provide instance methods invoked using the dot (.) operator.
2. The Architectural Principle of Immutability
Every String object in Java is strictly immutable. After a String instance is constructed in memory, its internal state—the sequence of characters it encapsulates—is permanent and cannot be modified under any circumstances.
Memory State During String Modification:
Step 1: String s = "java";
[s] ----------> [ String Object: "java" (@0x100) ]
Step 2: s = s.concat("8");
[s] --+ [ String Object: "java" (@0x100) ] (Unchanged!)
|
+-------> [ NEW String Object: "java8" (@0x200) ]
Why Did Java's Designers Make String Immutable?
- Security: Strings are used to transport critical data throughout applications, such as file paths, database connection URLs, network hostnames, and user credentials. If strings were mutable, an untrusted thread could alter a file path or URL after security checks were verified, causing severe security vulnerabilities (time-of-check to time-of-use exploits).
- Thread Safety: Immutable objects cannot be modified by any caller. Consequently, multiple concurrent threads can share identical
Stringinstances freely without synchronization locks, thread contention, or data corruption. - String Constant Pool Optimization: Caching and sharing literal strings across an entire JVM application is only safe if none of the consumers can alter the shared string. If strings were mutable, one method altering its string would silently corrupt the state of all other variables referencing that shared pooled literal.
- HashCode Caching: Because the character contents of a
Stringnever change, its hash code is computed once on demand and cached internally. This makesStringexceptionally fast and reliable as keys in hash-based collections such asHashMapandHashSet.
The Immutability Exam Trap
Exam 1Z0-811 frequently features questions where string manipulation methods are invoked, but the returned reference is discarded:
String title = "oracle";
title.toUpperCase(); // Returns a NEW String "ORACLE", but return value is ignored!
System.out.println(title); // Prints "oracle" (original object remains unchanged!)
// Correct approach: Reassign the reference to capture the new object
title = title.toUpperCase();
System.out.println(title); // Prints "ORACLE"
Remember: No method on String alters the calling object in place. If you do not capture the return value, the operation is lost.
3. The String Constant Pool (SCP)
Because applications typically contain vast quantities of repetitive string literals (labels, SQL queries, error messages), allocating a distinct heap object for every literal would rapidly deplete memory. To solve this, the JVM manages a dedicated cache inside heap memory called the String Constant Pool.
STACK (References) HEAP MEMORY
+-------------+ +--------------------------------+
| s1 | ------------> | String Constant Pool |
+-------------+ | |
| +----------------------+ |
+-------------+ | | "Java" | |
| s2 | ------------> | | (Shared Object) | |
+-------------+ | +----------------------+ |
+--------------------------------+
^
+-------------+ |
| s3 | ------------> [ New Heap Obj ]+
+-------------+ (Points to underlying pooled chars)
Literal Declaration vs. The new Keyword
-
String Literal Declaration (
String s = "Java";): When the JVM encounters a string literal, it searches the String Constant Pool:- If an identical character sequence already exists in the pool, the JVM returns a reference to that existing pooled object.
- If the literal is not present, a new
Stringobject is created and placed into the pool.
Therefore, multiple literal declarations with identical text share the exact same memory address:
String s1 = "Java"; String s2 = "Java"; System.out.println(s1 == s2); // true: both point to the identical pooled object -
Explicit Instantiation via
new String("Java");: Using thenewkeyword explicitly commands the JVM to bypass standard pooling. The JVM allocates a brand-newStringobject in general heap memory outside the pool, even if an identical literal already resides in the pool:String s3 = new String("Java"); System.out.println(s1 == s3); // false: different memory addresses!
The intern() Method
Invoking .intern() on a String object queries the String Constant Pool:
- If the pool contains an equal string, the reference from the pool is returned.
- If not, the string is added to the pool and that reference is returned.
String s1 = "Java";
String s3 = new String("Java");
String s4 = s3.intern(); // Retrieves the pooled canonical reference
System.out.println(s1 == s4); // true: s4 references the pooled object shared with s1
4. String Equality: == vs. .equals()
Understanding how to properly compare strings is tested extensively on Exam 1Z0-811.
| Comparison Technique | What It Tests | Evaluation Mechanism |
|---|---|---|
== Operator | Reference Equality | Evaluates whether both reference variables hold the identical memory address on the heap. |
.equals(Object obj) | Content Equality | Evaluates whether two strings contain the identical sequence of Unicode characters (case-sensitive). |
.equalsIgnoreCase(String str) | Content Equality | Evaluates character sequence equivalence, ignoring uppercase and lowercase differences. |
.compareTo(String anotherString) | Lexicographical Order | Returns an integer (< 0, 0, or > 0) representing alphabetical/Unicode sort order. |
Code Demonstration of Equality
String strA = "Oracle";
String strB = new String("Oracle");
String strC = "oracle";
// Reference equality comparison (tests memory addresses)
System.out.println(strA == strB); // false: different heap objects
// Content equality comparison (tests character sequence)
System.out.println(strA.equals(strB)); // true: identical characters 'O','r','a','c','l','e'
// Case-insensitive content comparison
System.out.println(strA.equalsIgnoreCase(strC)); // true: matches ignoring case
Exam Trap: Never use
==to compare string text. In practice,==only returnstrueif both variables happen to point to the exact same pooled literal or the same instantiated object. To evaluate textual equivalence, always invoke.equals().
Lexicographical Comparison with compareTo()
The compareTo(String anotherString) method performs character-by-character comparison based on Unicode numeric values:
- Returns
0if both strings contain identical character sequences. - Returns a negative integer if the calling string precedes the argument string lexicographically.
- Returns a positive integer if the calling string follows the argument string lexicographically.
System.out.println("Apple".compareTo("Banana")); // Negative: 'A' (65) < 'B' (66)
System.out.println("Dog".compareTo("Cat")); // Positive: 'D' (68) > 'C' (67)
System.out.println("Java".compareTo("Java")); // 0: identical
5. Compile-Time vs. Runtime String Concatenation
A sophisticated question type on Exam 1Z0-811 tests the difference between compile-time constant string expressions and runtime string concatenation.
Compile-Time Constant Expressions
If all operands in a string concatenation expression are compile-time constants (literals or final variables initialized with constant expressions), the Java compiler evaluates the concatenation during compilation and inserts the resulting combined literal into the String Constant Pool:
String base = "Java";
String pooled = "Java8";
String compiled = "Java" + "8"; // Both are literals: evaluated at compile-time as "Java8"
System.out.println(pooled == compiled); // true: both point to the same pooled "Java8" object!
Runtime String Concatenation
If any operand in the concatenation is a non-final variable or the result of a method call, the concatenation cannot be computed until runtime. The JVM generates a new StringBuilder to append the components, resulting in a new String object on the general heap outside the pool:
String prefix = "Java";
String runtimeCombined = prefix + "8"; // prefix is a variable: evaluated at runtime
System.out.println(pooled == runtimeCombined); // false: runtimeCombined is a new heap object!
System.out.println(pooled.equals(runtimeCombined)); // true: identical character content
Examine the following code segment:
What is printed to standard output?String greeting = "Hello";
greeting.concat(" World");
greeting.toUpperCase();
greeting.replace('H', 'J');
System.out.println(greeting);
Consider the following code statements:
What is the exact output printed to the console?String s1 = "Java";
String s2 = "Java";
String s3 = new String("Java");
String s4 = s3.intern();
System.out.print((s1 == s2) + " ");
System.out.print((s1 == s3) + " ");
System.out.print(s1.equals(s3) + " ");
System.out.print(s1 == s4);
Which of the following correctly identifies primary architectural reasons why Java's designers made the String class immutable?