Why String is immutable in Java

In Java, strings are immutable, which means their values cannot be changed after they are created. This design decision was made for several reasons.

  1. String Pooling for Performance: Immutability enables Java to use the “String Pool” technique, where string literals are pooled and reused, rather than creating new instances with the same value. This pooling mechanism can improve performance and save memory by reducing the number of duplicate string objects.

Let’s see an example.

public class StringPoolingExample {
    public static void main(String[] args) {
        // String literals are automatically pooled
        String str1 = "Hello";
        String str2 = "Hello";
        String str3 = "Hello";
        
        // Even though we used three different string references,
        // they all point to the same memory location in the string pool
        
        // Check if they refer to the same object in memory
        System.out.println("str1 == str2: " + (str1 == str2)); // true
        System.out.println("str1 == str3: " + (str1 == str3)); // true
        
        // Using the 'new' keyword to create a String object
        String str4 = new String("Hello");
        
        // 'str4' is a new object created on the heap, not in the string pool
        System.out.println("str1 == str4: " + (str1 == str4)); // false
        
        // However, content-wise they are the same
        System.out.println("str1.equals(str4): " + str1.equals(str4)); // true
    }
}

In this example, we create multiple string references (str1, str2, and str3) that point to the same string literal “Hello.” Since string literals are pooled, they all refer to the same memory location, and the equality check using == returns true for all of them.

We also demonstrate that creating a string using the new keyword (str4) results in a new object on the heap, not in the string pool. Therefore, the equality check using == with str1 and str4 returns false. However, str1.equals(str4) returns true because equals() compares the content of the strings, not their memory references.

By using string pooling, Java optimizes memory usage and improves performance by reusing string literals rather than creating new instances for each occurrence of the same value.

  1. Security: Strings are often used to store sensitive information like passwords or access tokens. Immutability ensures that once the string is created, its value cannot be changed accidentally or maliciously. This property helps prevent security vulnerabilities, such as data tampering or unauthorized access.

Let’s see an example of Java program to demonstrate how immutability helps in maintaining the security of sensitive information like passwords. In this example, we’ll simulate a scenario where a password is entered, stored as an immutable string, and then an attempt is made to modify it, showing that it remains unchanged due to immutability.

import java.util.Scanner;

public class SecureStringExample {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter your password: ");
        String password = scanner.nextLine();

        // Convert the password to an immutable string using intern()
        String immutablePassword = password.intern();

        // Now let's try to modify the original password and the immutablePassword
        password = "newPassword";
        // immutablePassword = "newImmutablePassword"; // This line will give a compile-time error

        // The above line is commented out because it would result in a compile-time error
        // trying to modify immutablePassword, but we know it would not be allowed.

        // Just to demonstrate that the immutablePassword remains unchanged
        System.out.println("Password: " + password);
        System.out.println("Immutable Password: " + immutablePassword);
    }
}

The output of above program

Enter your password: mySecretPassword
Password: newPassword
Immutable Password: mySecretPassword

In this program, we first ask the user to input their password. We then convert the password to an immutable string using the intern() method. Now, even if we try to modify the original password, it is allowed, but when we try to modify the immutablePassword, it will result in a compile-time error, showing that it cannot be changed due to immutability.

By using immutable strings to store sensitive information like passwords, we prevent accidental or malicious changes to this data, thus enhancing security and reducing the risk of data tampering or unauthorized access.

  1. Hashing and Caching Mechanisms: The immutability of strings allows them to be used as keys in hashing and caching mechanisms effectively. When you use a string as a key in a HashMap or HashSet, its hash code is calculated based on its content, and it remains constant throughout its lifetime. This consistency ensures that the key can be accurately retrieved from the collection, even if its value has not changed.

Let’s see an example.

import java.util.HashMap;
import java.util.Map;

public class StringHashingAndCachingExample {
    public static void main(String[] args) {
        // Create a HashMap with String keys and Integer values
        Map<String, Integer> hashMap = new HashMap<>();

        // Add elements to the HashMap
        String key1 = "apple";
        String key2 = "banana";
        String key3 = "orange";

        hashMap.put(key1, 10);
        hashMap.put(key2, 20);
        hashMap.put(key3, 30);

        // Retrieve values using the keys
        int value1 = hashMap.get(key1);
        int value2 = hashMap.get(key2);
        int value3 = hashMap.get(key3);

        System.out.println("Value associated with 'apple': " + value1);
        System.out.println("Value associated with 'banana': " + value2);
        System.out.println("Value associated with 'orange': " + value3);

        // Now let's try to change the keys and retrieve values again
        String modifiedKey1 = "pineapple"; // Changing the key for 'apple'

        // The HashMap will not find the value for 'pineapple' since it's a new key
        Integer newValue1 = hashMap.get(modifiedKey1);
        Integer newValue2 = hashMap.get(key2);
        Integer newValue3 = hashMap.get(key3);

        System.out.println("Value associated with 'pineapple': " + (newValue1 != null ? newValue1 : "Key not found"));
        System.out.println("Value associated with 'banana': " + newValue2);
        System.out.println("Value associated with 'orange': " + newValue3);
    }
}

The output of the above program is

Value associated with 'apple': 10
Value associated with 'banana': 20
Value associated with 'orange': 30
Value associated with 'pineapple': Key not found
Value associated with 'banana': 20
Value associated with 'orange': 30
  1. Class Loading and Constants: In Java, strings are often used as constants for class loading, switch statements, or other types of comparison. The immutability guarantees that these constants remain constant throughout the program’s execution, preventing unexpected behavior or bugs caused by accidentally modifying the constants.

Let’s see an example

public class StringConstantsExample {
    public static final String CONSTANT_NAME = "John Doe";
    public static final String GREETING = "Hello, ";

    public static void main(String[] args) {
        // Using strings as constants for class loading, switch statements, or comparison
        System.out.println(GREETING + CONSTANT_NAME);

        // Trying to modify the constant will result in a compilation error
        // Uncomment the line below to see the compilation error
        // CONSTANT_NAME = "Jane Doe";
    }
}

The output of the above program

Hello, John Doe

The program successfully prints the greeting message “Hello, John Doe” by concatenating the GREETING constant with the CONSTANT_NAME constant.

If you try to uncomment the line that attempts to modify the constant (CONSTANT_NAME = "Jane Doe";), the code will not compile, and you will receive a compilation error. The error message will be something like:

error: cannot assign a value to final variable CONSTANT_NAME
        CONSTANT_NAME = "Jane Doe";

  1. Functional Programming: In functional programming paradigms, immutability is highly valued as it makes functions more reliable and easier to reason about. Immutable strings align well with functional programming principles, enabling developers to write more robust and maintainable code.

Let’s see Java example that demonstrates how functional programming principles, along with immutability, can be applied to perform operations on immutable strings in a more reliable and maintainable way:

import java.util.function.Function;

public class FunctionalProgrammingExample {
    public static void main(String[] args) {
        String originalString = "Hello, world!";
        System.out.println("Original String: " + originalString);

        // Applying functional programming principles with immutability
        String transformedString = transformString(originalString, s -> s.toUpperCase());
        System.out.println("Transformed to Uppercase: " + transformedString);

        transformedString = transformString(originalString, s -> s.replaceAll("world", "Functional Programming"));
        System.out.println("Transformed with Replacement: " + transformedString);

        // Original string remains unchanged after transformations
        System.out.println("Original String (after transformations): " + originalString);
    }

    public static String transformString(String input, Function<String, String> transformation) {
        return transformation.apply(input);
    }
}

The output of the above program

Original String: Hello, world!
Transformed to Uppercase: HELLO, WORLD!
Transformed with Replacement: Hello, Functional Programming!
Original String (after transformations): Hello, world!

In this example, we define a transformString method that takes an input string and a functional interface Function<String, String>. The Function represents an operation to transform the input string into a new string. Since strings are immutable in Java, the transformations create new string objects instead of modifying the original string.

In the main() method, we demonstrate how functional programming principles can be applied to perform different transformations on the immutable string originalString. We use lambda expressions to define the transformations concisely.

The program first prints the originalString, then applies two different transformations on it: converting it to uppercase and replacing the word “world” with “Functional Programming”. The result of each transformation is stored in the transformedString variable, and the original originalString remains unchanged.

As you can see, functional programming principles, combined with immutability, allow us to create more reliable and maintainable code. The original string is preserved, and each transformation produces a new string, ensuring that the functions remain pure without any side effects.

Overall, immutability in strings promotes better code design, reduces bugs related to shared references, enhances thread safety, and provides better performance and security characteristics.

In summary, the immutability of strings in Java brings benefits like memory efficiency, improved performance, thread safety, enhanced security, and simplified programming models. These advantages make the use of immutable strings a crucial design choice in the Java language and ecosystem.

That’s all about Why String is immutable in Java.