Java 中 String 不可变或 final 的原因?

2025年3月27日 | 阅读 4 分钟

在面向对象编程中,不可变字符串或对象一旦创建就不能被修改。但我们只能改变对象的引用。我们限制改变对象本身。String 在 Java 中之所以不可变,是因为安全性、同步和并发、缓存以及类加载。这种不可变性是通过将字符串声明为 final 来实现的,确保一旦创建,它们就不能被修改。使 String final 的原因是为了破坏可变性,并且不让其他人继承它。

String 对象缓存在 String 池中,这使得 String 不可变。缓存的 String 字面量被多个客户端访问。因此,总有一个风险,一个客户端执行的操作会影响所有其他客户端。虽然在 String 池中缓存 String 字面量可以提高性能,但在多客户端环境中,也存在意外后果的风险。

例如,如果一个客户端执行一个操作并将字符串值从 Pressure 更改为 PRESSURE,所有其他客户端也将读取该值。出于性能原因,缓存 String 对象很重要,为了消除这种风险,我们必须使 String 不可变。

Why String is Immutable or Final in Java

以下是使 String 不可变的原因:

  • 线程安全:考虑一个场景,多个线程正在并发地访问和修改一个共享的字符串对象。

如果没有不可变性,这种并发修改可能会导致竞态条件和数据损坏。通过使字符串不可变,Java 确保了线程安全,因为每个线程都操作其字符串的副本,从而防止了干扰。

  • 如果我们不使 String 不可变,它将对应用程序构成严重的 **安全威胁**。例如,数据库用户名、密码以字符串形式传递以接收数据库连接。套接字编程的主机和端口描述也以字符串形式传递。String 是不可变的,因此其值无法更改。如果 String 不保持不可变,任何黑客都可以通过更改引用值来导致应用程序的安全问题。

字符串池和内存效率

示例:Java 中会自动对字符串字面量进行 intern 操作,从而实现字符串池。

不可变性使 JVM 能够重用现有的字符串实例,从而减少内存开销并提高内存使用效率。这在处理大量字符串数据时尤其有利。

  • 如果 Java 中 String 不可变,那么 String 池将不可能存在。JRE 节省了大量的堆空间。池中的多个字符串变量可以引用相同的字符串变量。如果 String 不可变,String interning 也不可能实现。

安全性

示例:考虑将密码等敏感信息存储为字符串。

不可变性确保一旦敏感信息被赋给字符串,其值就无法被无意修改。这有助于减轻与无意修改或敏感数据泄露相关的安全风险。

  • 由于其不可变性,String 对多线程是安全的。不同的线程可以访问单个“String 实例”。由于我们隐式地使字符串线程安全,因此消除了线程安全的同步。

性能优化

示例:执行字符串连接。

虽然连接可变字符串涉及在每一步创建新的字符串对象,但不可变字符串允许 JVM 通过内部使用 StringBuilder 来优化此操作。这可以提高性能并减少内存消耗。

  • 不可变性为 Classloader 加载正确类提供了安全性。例如,假设我们有一个实例,我们尝试加载 `java.sql.Connection` 类,但对 `myhacked.Connection` 类的引用值的更改会对我们的数据库产生不必要的影响。

让我们通过一个例子来理解不可变的概念。

ImmutableString.java

输出

Why String is Immutable or Final in Java

描述:我们可以借助下图来理解上面的例子

Why String is Immutable or Final in Java

在字符串常量池中,**Hello** 保持不变,并创建了一个新的字符串对象 **HelloWorld**。这表明字符串是不可变的。引用变量指向 **Hello** 而不是 **HelloWorld**。

如果我们想让它指向 **HelloWorld**,我们必须显式地将其分配给该变量。例如

ImmutableString.java

输出

Why String is Immutable or Final in Java

Java 中,不可变性由 final 关键字强制执行,是语言设计的一个基石。通过防止字符串对象一旦创建就无法修改,Java 确保了线程安全,通过字符串池提高了内存使用效率,增强了安全性,并实现了性能优化。虽然对于习惯了可变字符串的开发人员来说,这可能需要转变思维方式,但采用不可变性最终会带来更健壮、更可靠的 Java 应用程序。