Java 编程中的不可变字符串

JavaJavaBeginner
立即练习

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

引言

在本实验中,你将学习在 Java 编程中使用不可变(immutable)字符串(String)的好处。你将理解为什么 Java 中的字符串默认是不可变的,以及这一特性如何提升你应用程序的整体性能和安全性。完成本实验后,你将能够创建并有效地在你的 Java 程序中使用不可变字符串。

创建 Java 项目文件

在此步骤中,你将在 ~/project 目录中创建一个名为 ImmutableStringDemo.java 的新 Java 文件。该文件将包含本实验的所有 Java 代码。

首先,请确保你位于 ~/project 目录。你可以使用 pwd 命令来验证你当前所在的目录:

pwd

你应该会看到类似以下的输出:

/home/labex/project

现在,使用 touch 命令创建 ImmutableStringDemo.java 文件:

touch ImmutableStringDemo.java

创建文件后,你可以在 WebIDE 编辑器中打开它,开始编写你的 Java 代码。

理解字符串的不可变性与字符串常量池

在 Java 中,String 对象是不可变的(immutable),这意味着一旦创建,它们的值就无法被更改。这一设计选择带来了多项优势,包括安全性、线程安全以及通过字符串常量池(String pool)实现的性能优化。

字符串常量池是堆(heap)内存中的一个特殊区域,用于存储字符串字面量(String literals)。当你创建一个字符串字面量时,Java 会首先检查常量池中是否已存在内容相同的字符串。如果存在,则返回已存在的字符串的引用,从而节省内存。如果不存在,则会在常量池中创建一个新的字符串并返回其引用。

接下来,我们添加一个基本的 Java 类结构并演示字符串的创建。在 WebIDE 中打开 ImmutableStringDemo.java 文件,并添加以下代码:

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

    public static void main(String[] args) {
        // 创建一个字符串字面量
        String name = "LabexUser";
        System.out.println("Initial name: " + name);

        // 创建可能使用字符串常量池的字符串
        String str1 = "Hello";
        String str2 = "Hello"; // 在字符串常量池中,这很可能指向与 str1 相同的对象

        System.out.println("str1: " + str1);
        System.out.println("str2: " + str2);
        System.out.println("str1 == str2: " + (str1 == str2)); // 检查它们是否指向同一个对象

        // 使用 'new' 关键字创建字符串
        // 即使字面量已存在于常量池中,这也会始终在堆中创建一个新对象
        String str3 = new String("Hello");
        System.out.println("str3: " + str3);
        System.out.println("str1 == str3: " + (str1 == str3)); // 检查它们是否指向同一个对象
    }
}

添加代码后,请保存文件。

现在,编译并运行 Java 程序以查看输出。在终端中,执行以下命令:

javac ImmutableStringDemo.java
java ImmutableStringDemo

你应该会看到类似以下的输出:

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

此输出表明 str1str2 指向字符串常量池中的同一个对象,而 str3(使用 new String() 创建)是堆中的一个独立对象,尽管它们的内容相同。

为安全和线程安全强制执行不可变性

在 Java 中,不可变性对于安全和线程安全至关重要。当一个字符串是不可变的,它的值在创建后就无法被程序的任何部分更改。这可以防止恶意代码修改存储在字符串中的敏感数据,例如密码或 URL。

此外,不可变对象本身就是线程安全的。多个线程可以并发访问一个不可变的字符串,而无需外部同步,因为不存在一个线程在另一个线程读取时修改该字符串的风险。

让我们通过为字符串添加 final 关键字并创建一个简单的线程安全示例来演示这一点。

请按如下方式修改你的 ImmutableStringDemo.java 文件。我们将声明一个类级别的 final 字符串,使其可以被我们的 ThreadSafeTask 类访问。

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

public class ImmutableStringDemo {

    // 声明一个 final 字符串以确保安全和线程安全
    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) {
        // ... (name, str1, str2, str3 的先前代码保留在此处) ...
        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 ---");

        // 使用 final 字符串
        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();
        }
    }
}

注意: final 关键字确保 SECURE_PASSWORD 变量只能被赋值一次。由于 String 本身是不可变的,这使得 SECURE_PASSWORD 的内容无法被更改。ThreadSafeTask 类可以安全地从多个线程访问这个不可变的字符串。

保存文件。然后,再次编译并运行程序:

javac ImmutableStringDemo.java
java ImmutableStringDemo

你应该会看到类似以下的输出,线程访问消息的顺序可能不是确定的:

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

利用不可变性实现集合中的哈希码缓存

字符串的不可变性使其成为 HashMapHashSet 等基于哈希的集合中用作键的理想选择。由于字符串的值永远不会改变,因此它的 hashCode() 可以计算一次并进行缓存。后续对 hashCode() 的调用将返回缓存的值,从而带来显著的性能提升,尤其是在字符串频繁用作键时。

如果字符串是可变的,那么在将其插入 HashMap 后,它们的哈希码可能会发生变化,导致无法使用原始哈希码检索对象。

让我们向 ImmutableStringDemo.java 添加代码,以演示在 HashMap 中使用字符串。

ImmutableStringDemo.java 文件的 main 方法中,在线程安全演示之后添加以下代码:

// ~/project/ImmutableStringDemo.java
import java.util.HashMap; // 确保此导入位于文件顶部

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) {
        // ... (name, str1, str2, str3 以及线程安全之前的代码保留在此处) ...
        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"; // 这将指向字符串常量池中的同一个 "Alice"

        studentScores.put(studentName1, 95);
        studentScores.put(studentName2, 88);
        studentScores.put(studentName3, 92); // 这将更新 "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"));

        // 演示即使我们尝试“更改”一个字符串,也会创建一个新的字符串
        String originalString = "Java";
        System.out.println("Original String: " + originalString + ", HashCode: " + originalString.hashCode());

        String modifiedString = originalString.concat(" Programming"); // 创建一个新的 String
        System.out.println("Modified String: " + modifiedString + ", HashCode: " + modifiedString.hashCode());
        System.out.println("Original String (after concat): " + originalString + ", HashCode: " + originalString.hashCode());
    }
}

保存文件。然后,再次编译并运行程序:

javac ImmutableStringDemo.java
java ImmutableStringDemo

你应该会看到类似以下的输出,包括 HashMap 和哈希码演示:

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

请注意,即使在调用 concatoriginalString 的哈希码保持不变,因为 concat 返回的是一个 新的 String 对象,而原始对象保持不变。

总结

在本实验中,你学习了 Java 编程中不可变字符串的基本概念。你探讨了字符串默认不可变的原因,以及这一设计选择如何通过字符串常量池(String pool)实现内存优化。你还理解了不可变性的关键优势,包括增强敏感数据的安全性、并发操作的固有线程安全以及由于哈希码缓存而提升的基于哈希的集合的性能。通过完成实践练习,你获得了在 Java 应用程序中有效创建和使用不可变字符串的实践经验。