Phase 3
Strings, Object Contract & Memory Internals
📂 Module 1... \\ 02_Arrays_Strings_Demos & Module 2 \\ 01_Java11_Demos
3.1 String Immutability, String Constant Pool (SCP) & Memory Mechanics
📖 Cay S. Horstmann • Core Java Volume I — Chapter 3, Section 3.6 ("Strings") • Chapter 5 ("Object Contract")
“The Java designers decided to share strings. No method in the String class can change a string. A string is an immutable sequence of Unicode characters... The equals method determines whether two objects have identical state.”
- SCP FlyweightString literals reside in the String Constant Pool in Heap memory for maximum sharing.
⚡ Pro-Tip: equals() & hashCode() Sacred Contract
If a.equals(b) is true, then a.hashCode() == b.hashCode() MUST be true! Always generate both methods together in your IDE using the exact same business key fields.
💡 Real-World Analogy: Why Strings are Immutable (Currency Engraving)
Imagine you have a $10 bill. If you want $20, you cannot take a red pen and scribble a '2' over the '1' to change its value. That would cause chaos and counterfeiting! Instead, you must receive an entirely distinct, newly minted $20 bill.
In Java,
• When you do
• It leaves "Hello" completely untouched in memory, and mints a brand-new String object
In Java,
String is designed the same way:
• When you do
String s = "Hello"; s = s + " World";, Java does NOT change "Hello".
• It leaves "Hello" completely untouched in memory, and mints a brand-new String object
"Hello World" on the Heap!
🖊 Architectural Memory Layout: String Pool vs new String() Heap Instances
Mermaid Diagram
graph TD
subgraph StackSpace ["Thread Stack"]
s1["s1 = 'Java'"]
s2["s2 = 'Java'"]
s3["s3 = new String('Java')"]
end
subgraph HeapSpace ["Java Heap Memory"]
subgraph SCP ["String Constant Pool (SCP)"]
PoolLiteral["'Java' (Single interned instance @ 0x100)"]
end
HeapObj["Heap Object: new String() @ 0x500
value -> points to Pool instance"] end s1 --> PoolLiteral s2 --> PoolLiteral s3 --> HeapObj HeapObj -.-> PoolLiteral
value -> points to Pool instance"] end s1 --> PoolLiteral s2 --> PoolLiteral s3 --> HeapObj HeapObj -.-> PoolLiteral
📂 StringMemoryDemo.java — Literal vs new String()
public class StringMemoryDemo {
public static void main(String[] args) {
// String Literals: Reuses the single instance in String Constant Pool (SCP)
String s1 = "Java";
String s2 = "Java";
// 'new' Operator: FORCES a brand-new object on Heap outside SCP
String s3 = new String("Java");
// == compares MEMORY ADDRESS POINTERS
System.out.println(s1 == s2); // true (Both point to same SCP address!)
System.out.println(s1 == s3); // false (Different Heap addresses!)
// .equals() compares CHARACTER CONTENTS
System.out.println(s1.equals(s3)); // true (Both hold characters 'J','a','v','a')
// .intern() moves/finds the reference in the String Constant Pool
String s4 = s3.intern();
System.out.println(s1 == s4); // true (Now pointing to same SCP instance!)
}
}
👁 4 Core Reasons Why Strings Are Immutable in Java
-
1. Memory Saving
The String Constant Pool (SCP) allows millions of identical string literals across your application to share a single Heap instance. If strings were mutable, changing
s1would secretly corrupts2! - 2. Thread Safety Immutable objects can be shared freely across 500 concurrent worker threads with ZERO locks and ZERO synchronization overhead.
-
3. Security
File paths, network socket URLs, and database connection strings are passed as Strings. If Strings were mutable, an attacker could pass a valid file path, bypass validation, and then mutate the path to
/etc/shadowon another thread! -
4. HashCode Caching
Because a String's content never changes, its
hashCode()is calculated once lazily and cached. This makesHashMaplookups with String keys lightning fast.
3.2 The equals() and hashCode() Contract (The Library Analogy)
💡 Real-World Analogy: Finding a Book in a Giant Library
Imagine you walk into a library with 1,000,000 books looking for "Core Java Vol I".
• hashCode() is the Aisle Number: It tells the librarian: "Check Aisle #42". Instead of scanning all 1,000,000 books one-by-one, you instantly jump to Aisle #42 (O(1) time!).
• equals() is the Title Verification: Once inside Aisle #42, there might be 5 books on the shelf. You check each book's title to find the exact match.
THE DISASTER OF BREAKING THE CONTRACT:
If you override
You put a book into the library. Later, you create an identical book with the same title to look it up. But because
You walk into Aisle #99 and search every book. The librarian says: "Book not found!" — even though the exact book is sitting in Aisle #42! That is why your
• hashCode() is the Aisle Number: It tells the librarian: "Check Aisle #42". Instead of scanning all 1,000,000 books one-by-one, you instantly jump to Aisle #42 (O(1) time!).
• equals() is the Title Verification: Once inside Aisle #42, there might be 5 books on the shelf. You check each book's title to find the exact match.
THE DISASTER OF BREAKING THE CONTRACT:
If you override
equals() but FORGET to override hashCode():
You put a book into the library. Later, you create an identical book with the same title to look it up. But because
hashCode() was default, it generates a different aisle number (Aisle #99)!
You walk into Aisle #99 and search every book. The librarian says: "Book not found!" — even though the exact book is sitting in Aisle #42! That is why your
HashMap.get(key) returns null when the contract is broken.
📂 ImmutableEmployee.java — Complete Implementation
package com.accenture.ltt.oop;
import java.util.*;
/**
* Standard Production Immutable Class Design:
* 1. Declare class 'final' to prevent subclasses from adding mutable state.
* 2. All fields are 'private final'.
* 3. No setter methods.
* 4. Perform defensive copying in constructor for mutable objects (Lists, Dates).
* 5. Perform defensive copying in getters.
*/
public final class ImmutableEmployee {
private final String empId;
private final List<String> skills;
public ImmutableEmployee(String empId, List<String> skills) {
this.empId = empId;
// Defensive copy in constructor: Prevents caller from mutating our internal list!
this.skills = new ArrayList<>(skills);
}
public String getEmpId() { return empId; }
public List<String> getSkills() {
// Return unmodifiable view: Prevents caller from adding elements to our list!
return Collections.unmodifiableList(skills);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutableEmployee that = (ImmutableEmployee) o;
return Objects.equals(empId, that.empId);
}
@Override
public int hashCode() {
return Objects.hash(empId);
}
}
👁 The 5 Golden Rules of Immutability & Defensive Copying
-
Rule 1: final Class
public final class ImmutableEmployeeprevents malicious or accidental subclassing that could override getters and introduce mutable state. - Rule 2 & 3: private final Fields & No Setters Fields are initialized once during construction and can never be reassigned. Absence of setters guarantees state is fixed for the lifetime of the object.
-
Rule 4: Defensive Copy in Constructor
Line 8:
this.skills = new ArrayList<>(skills);— If you wrotethis.skills = skills;, the caller would keep the original list pointer outside the class and could add elements (skills.add("Hacker")), quietly mutating your "immutable" object! Defensive copying clones the data into a private internal list. -
Rule 5: Defensive Copy in Getters
Line 15:
return Collections.unmodifiableList(skills);— Never return raw references to mutable collections. Returning an unmodifiable wrapper guarantees anyone callinggetSkills().add(...)receives an immediateUnsupportedOperationException.
📂 Module 2.../01_Java11_Demos/.../Tester1005_String_Strip.java
package com.accenture.ltt.zeleven;
public class Tester1005_String_Strip {
public static void main(String[] args) {
String name = " Mohpreet ";
// 1. strip(): Unicode-aware removal of leading and trailing whitespace
System.out.println("strip(): [" + name.strip() + "]");
// 2. stripLeading(): Removes only leading whitespaces
System.out.println("stripLeading(): [" + name.stripLeading() + "]");
// 3. stripTrailing(): Removes only trailing whitespaces
System.out.println("stripTrailing(): [" + name.stripTrailing() + "]");
// 4. isBlank(): Checks if empty or contains only whitespace codepoints
System.out.println("\" \".isBlank(): " + " ".isBlank()); // true (isEmpty() would be false!)
// 5. repeat(count): Concatenates string N times without manual loop
System.out.println("Java ".repeat(3)); // "Java Java Java "
}
}
👁 Java 11 Modern String Enhancements: strip() vs trim()
-
strip() vs trim()
trim()was introduced in Java 1.0 and only detects ASCII whitespace (characters ≤ U+0020).strip()is the modern Unicode-aware replacement usingCharacter.isWhitespace(c), properly stripping non-breaking spaces and international Unicode whitespace. -
isBlank() vs isEmpty()
isEmpty()checks iflength == 0." ".isEmpty()returnsfalse!isBlank()accurately identifies strings consisting purely of whitespace characters, eliminating manual regex or Apache Commons utility dependencies.
📂 Module 1.../Part-1-Demos/.../TracingWithLogger.java
package com.accenture.ltt;
import java.util.logging.Logger;
import java.util.logging.Level;
public class TracingWithLogger {
// Thread-safe static logger instance bound to class name
private static final Logger LOG = Logger.getLogger(TracingWithLogger.class.getName());
public static void main(String[] args) {
LOG.setLevel(Level.ALL); // Capture all tracing thresholds
LOG.info("=== Enterprise Tracing With Logger START ===");
int[] numbers = {2, -1, 3};
int total = process(numbers);
LOG.info("Final computed total = " + total);
LOG.info("=== Execution Finished Successfully ===");
}
static int process(int[] arr) {
LOG.info("process() called with array length=" + arr.length);
int sum = 0;
for (int i = 0; i < arr.length; i++) {
LOG.fine("At index " + i + ", value=" + arr[i]);
sum = add(sum, arr[i]);
}
return sum;
}
static int add(int a, int b) {
if (b < 0) {
LOG.warning("add() received negative parameter: " + b); // Highlight potential business anomaly
} else {
LOG.info("add() adding " + b);
}
return a + b;
}
}
👁 Production Logging Levels & Diagnostic Tracing
-
Logger vs System.out.println
Never use
System.out.printlnin enterprise code.System.outcauses thread blocking on stdout streams, cannot be filtered dynamically, and lacks timestamps, thread identifiers, or log level severities. -
Log Level Hierarchy
SEVERE(System crash/fatal) >WARNING(Recoverable anomaly) >INFO(Normal operational milestone) >FINE/DEBUG(Diagnostic execution steps). In production, set level toINFOto keep log volume minimal; set toFINEduring diagnostic investigations.