很多大厂面试,都会经常问到字符串相关的问题。
本篇,我们通过源码图文,来详细解析字符串的不可变性。
在 Java 编程中,String 类型是经常要用到的一种数据类型。在很多开发场景中,我们需要不断的去更新、改变 String 类型的值。
但是,在操作 String 类型的数据时,明明就做了大量的改变,为什么还说 String 类型是不可变的呢?
要想搞明白这个问题,我们有必要先理解下 Java 中所谓的不可变,到底是指什么。
Java 中不可变是指什么?
在 Java 中,不可变对象是指在对象创建完成之后其内部状态不会发生变化的对象。
也就是说,一旦这个对象被赋值之后,就不能更新这个对象的引用,也不能改变它对应的值了。
我们来看个实例。
String str = “hello” str = str.concat("world")
上面这个操作,就是对 String 类型的对象做了改变吧?得到的 str 字符串,不就变成了 “helloworld” 了吗?
在学习 Java 基础时,我们曾分析过 Java 的内存模型,再来温顾下这张图。
上述代码实例,看起来是 hello 变成了 helloworld ,但实际上是创建了一个新的对象。
因为只要一个 String 对象被创建,它就一定是不能被修改的,并且 String 类中的所有对于字符串的修改方法,都不是改变对应字符串本身的值,而是返回了一个新的对象。
也就是说,想要创建一个可变的字符串,通过 String 类型肯定是行不通的。我们在使用的过程中创建可变字符串,都是通过 StringBuffer 、或 StringBuilder 来代替 String 类型对象。
String 类型为什么是不可变的呢?
通过上述分析,我们知道了 String 类型是不可变的。
那么,为什么要这样做呢?
String 类型不可变的主要原因,主要归纳为以下四点,下面逐一详解。
缓存
在 C 语言、或C++语言中,对字符串的操作非常消耗资源。因此,在 Java 语言设计时,就采用了缓存操作,来提升堆内存空间的使用率。
在 JVM 中,也特意为字符串类型开辟了存储空间,就是字符串常量池。
通过这样的操作,当出现两个内容相同的字符串变量时,就可以指向字符串常量池中的同一个内存地址,能节省很多空间。
我们来看看这张图:
安全性
我们在开发应用时,通常会把一些重要的信息、使用字符类型进行存储;在类加载过程中,对于类的全路径,也是通过字符串进行存储的。
对 String 类型安全性的提升,也就显得尤其重要了。
我们在操作一个字符串的过程中:
- 如果这个字符串是不可变的,在操作过程中,我们可以认为,这个字符串就是原来那个字符串。
- 如果字符串是可变的,在操作过程中,我们有可能对字符串信息进行修改,这样就会导致,我们最终获取到数据,并不是所要操作的数据信息。
线程安全性
不可变的共享 String ,是线程安全的。
因为在多个线程共同访问时,线程对它的操作,都不会改变它的值,就形成了天然的线程安全。
通常,一个不可变的对象在线程之间共享的时候,这个不可变对象,一定是线程安全的。如果其中有一个线程改变了对象的值,在字符串常量池中,就会出现一个新的字符串,而不是有一个相同的值对象。
因此,字符串操作在多线程中,是线程安全的。
HashCode 识别
在很多情况下,我们在使用 Map 这样的存储结构时,会将 Key 的值设置为 String 类型的对象。
在操作这些对象时,就需要调用 hashCode() 计算对象的哈希值,由于字符串的不可变性,就保证了字符串的值不会发生变化。
所以,当 hashCode() 方法在 String 类型中进行了重写代码:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
第一次调用后,和之后的调用,所计算得到的哈希值都是一样的,这也就确保了唯一性。
性能方面
在性能方面,字符串常量池、使用缓存等操作,都是性能的体现。
此处就不延伸说明了。
总结
通过本篇,我们了解了String 字符串不可变的底层原因。
因为 String 类型被 final 标记,所以从源码层面上是不可变的,可以将字符串的引用看作成一个普通变量,但字符串本身是不可变的。