Immutable Strings in Java Programming

JavaBeginner
Practice Now

Introduction

In this lab, you will learn about the benefits of using immutable Strings in Java programming. You will understand why Strings in Java are immutable by default and how this characteristic can enhance the overall performance and security of your applications. By the end of this lab, you will be able to create and effectively use immutable Strings in your Java programs.

Create a Java Project File

In this step, you will create a new Java file named ImmutableStringDemo.java in your ~/project directory. This file will contain all the Java code for this lab.

First, ensure you are in the ~/project directory. You can verify your current directory using the pwd command:

pwd

You should see output similar to this:

/home/labex/project

Now, create the ImmutableStringDemo.java file using the touch command:

touch ImmutableStringDemo.java

After creating the file, you can open it in the WebIDE editor to start writing your Java code.

Understand String Immutability and the String Pool

In Java, String objects are immutable, meaning their value cannot be changed after they are created. This design choice offers several benefits, including security, thread safety, and performance optimizations through the String pool.

The String pool is a special memory area within the heap that stores String literals. When you create a String literal, Java first checks if an identical String already exists in the pool. If it does, the existing String's reference is returned, saving memory. If not, a new String is created in the pool and its reference is returned.

Let's add the basic structure of a Java class and demonstrate String creation. Open ImmutableStringDemo.java in the WebIDE and add the following code:

// ~/project/ImmutableStringDemo.java
public class ImmutableStringDemo {

    public static void main(String[] args) {
        // Creating a String literal
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        // Creating Strings that might use the String pool
        String str1 = "Hello";
        String str2 = "Hello"; // This will likely refer to the same object as str1 in the String pool

        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2)); // Checks if they refer to the same object

        // Creating a String using the 'new' keyword
        // This always creates a new object in the heap, even if the literal exists in the pool
        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3)); // Checks if they refer to the same object
    }
}

Save the file after adding the code.

Now, compile and run the Java program to see the output. In the terminal, execute the following commands:

javac ImmutableStringDemo.java
java ImmutableStringDemo

You should see output similar to this:

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

This output demonstrates that str1 and str2 refer to the same object in the String pool, while str3 (created with new String()) is a distinct object in the heap, even though its content is identical.

Enforce Immutability for Security and Thread Safety

Immutability is crucial for security and thread safety in Java. When a String is immutable, its value cannot be changed by any part of the program after creation. This prevents malicious code from altering sensitive data stored in Strings, such as passwords or URLs.

Furthermore, immutable objects are inherently thread-safe. Multiple threads can access an immutable String concurrently without needing external synchronization, as there's no risk of one thread modifying the String while another is reading it.

Let's demonstrate this by adding a final keyword to a String and creating a simple thread-safe example.

Modify your ImmutableStringDemo.java file as follows. We will declare a final String at the class level to make it accessible to our ThreadSafeTask class.

// ~/project/ImmutableStringDemo.java
import java.util.HashMap;

public class ImmutableStringDemo {

    // Declare a final String for security and thread safety
    private static final String SECURE_PASSWORD = "MySecurePassword123";

    // Inner class to demonstrate thread safety
    static class ThreadSafeTask implements Runnable {
        private final String password; // The password is passed as an immutable String

        public ThreadSafeTask(String password) {
            this.password = password;
        }

        @Override
        public void run() {
            System.out.println("Thread " + Thread.currentThread().getName() + " accessing password: " + password);
            // Simulate some work
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        // ... (previous code for name, str1, str2, str3 remains here) ...
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2));

        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3));

        System.out.println("\n--- Demonstrating Security and Thread Safety ---");

        // Using the final String
        System.out.println("Secure Password: " + SECURE_PASSWORD);

        // Demonstrate thread safety with the immutable password
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new ThreadSafeTask(SECURE_PASSWORD), "Thread-" + (i + 1));
            thread.start();
        }

        // Wait for threads to finish (optional, for cleaner output)
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Note: The final keyword ensures that the SECURE_PASSWORD variable can only be assigned once. Since String itself is immutable, this makes the content of SECURE_PASSWORD unchangeable. The ThreadSafeTask class safely accesses this immutable String from multiple threads.

Save the file. Then, compile and run the program again:

javac ImmutableStringDemo.java
java ImmutableStringDemo

You should see output similar to this, with the thread access messages appearing in a non-deterministic order:

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

--- Demonstrating Security and Thread Safety ---
Secure Password: MySecurePassword123
Thread Thread-1 accessing password: MySecurePassword123
Thread Thread-2 accessing password: MySecurePassword123
Thread Thread-3 accessing password: MySecurePassword123

Leverage Immutability for Hashcode Caching in Collections

The immutability of Strings makes them ideal for use as keys in hash-based collections like HashMap and HashSet. Because a String's value never changes, its hashCode() can be computed once and cached. Subsequent calls to hashCode() will return the cached value, leading to significant performance improvements, especially when Strings are frequently used as keys.

If Strings were mutable, their hash code could change after being inserted into a HashMap, making it impossible to retrieve the object using the original hash code.

Let's add code to ImmutableStringDemo.java to demonstrate String usage in a HashMap.

Add the following code to the main method of your ImmutableStringDemo.java file, after the thread safety demonstration:

// ~/project/ImmutableStringDemo.java
import java.util.HashMap; // Ensure this import is at the top of your file

public class ImmutableStringDemo {

    private static final String SECURE_PASSWORD = "MySecurePassword123";

    static class ThreadSafeTask implements Runnable {
        private final String password;

        public ThreadSafeTask(String password) {
            this.password = password;
        }

        @Override
        public void run() {
            System.out.println("Thread " + Thread.currentThread().getName() + " accessing password: " + password);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        // ... (previous code for name, str1, str2, str3, and thread safety remains here) ...
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        String str1 = "Hello";
        String str2 = "Hello";
        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2));

        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3));

        System.out.println("\n--- Demonstrating Security and Thread Safety ---");
        System.out.println("Secure Password: " + SECURE_PASSWORD);

        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new ThreadSafeTask(SECURE_PASSWORD), "Thread-" + (i + 1));
            thread.start();
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("\n--- Demonstrating Hashcode Caching ---");

        HashMap<String, Integer> studentScores = new HashMap<>();

        String studentName1 = "Alice";
        String studentName2 = "Bob";
        String studentName3 = "Alice"; // This will refer to the same "Alice" in the String pool

        studentScores.put(studentName1, 95);
        studentScores.put(studentName2, 88);
        studentScores.put(studentName3, 92); // This will update the value for "Alice"

        System.out.println("Student Scores: " + studentScores);
        System.out.println("Alice's score: " + studentScores.get("Alice"));
        System.out.println("Bob's score: " + studentScores.get("Bob"));

        // Demonstrate that even if we try to "change" a String, a new one is created
        String originalString = "Java";
        System.out.println("Original String: " + originalString + ", HashCode: " + originalString.hashCode());

        String modifiedString = originalString.concat(" Programming"); // Creates a new String
        System.out.println("Modified String: " + modifiedString + ", HashCode: " + modifiedString.hashCode());
        System.out.println("Original String (after concat): " + originalString + ", HashCode: " + originalString.hashCode());
    }
}

Save the file. Then, compile and run the program again:

javac ImmutableStringDemo.java
java ImmutableStringDemo

You should see output similar to this, including the HashMap and hashcode demonstrations:

Initial name: LabexUser
str1: Hello
str2: Hello
str1 == str2: true
str3: Hello
str1 == str3: false

--- Demonstrating Security and Thread Safety ---
Secure Password: MySecurePassword123
Thread Thread-1 accessing password: MySecurePassword123
Thread Thread-2 accessing password: MySecurePassword123
Thread Thread-3 accessing password: MySecurePassword123

--- Demonstrating Hashcode Caching ---
Student Scores: {Bob=88, Alice=92}
Alice's score: 92
Bob's score: 88
Original String: Java, HashCode: 2301506
Modified String: Java Programming, HashCode: -1479700901
Original String (after concat): Java, HashCode: 2301506

Notice how originalString's hash code remains the same even after concat is called, because concat returns a new String object, leaving the original untouched.

Summary

In this lab, you have learned about the fundamental concept of immutable Strings in Java programming. You explored why Strings are immutable by default and how this design choice contributes to memory optimization through the String pool. You also understood the critical benefits of immutability, including enhanced security for sensitive data, inherent thread safety for concurrent operations, and improved performance in hash-based collections due to hashcode caching. By completing the practical exercises, you have gained hands-on experience in creating and utilizing immutable Strings effectively in your Java applications.