String为什么是不可变的?

时间:2021-01-13 15:16:30

面试官Q1:请问为什么String是不可变的,能谈谈吗?

我们知道不管是面试初级、中级还是高级Java开发工程师,String永远都是一个绕不开的话题,而且问的问题也是各不相同,下面我们从几个角度来看看为什么String是不可变的?

什么是不可变对象?

   如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

我们来看下面一段代码:

public class Demo {
String str = "ABC";
System.out.println("s = " + str); str = "123";
System.out.println("s = " + str);
}

打印结果为:

1s = ABC
2s = 123

对于上述代码,我们简单的分析一下:首先创建一个String对象str,然后让str的值为“ABC”,然后又让str的值为“123”。从打印结果可以看出,str的值确实改变了。

那还说String对象是不可变的呢?

这里存在一个误区:str只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,放在堆中,成员变量越多,这块内存区占的空间越大。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象,而这个引用存放在Java虚拟机栈栈帧的局部变量表中。也就是说,str只是一个引用,它指向了一个具体的对象,当str=“123”; 这句代码执行过之后,又创建了一个新的对象“123”, 而引用str重新指向了这个新的对象,原来的对象“ABC”还在内存中存在,并没有改变。

我们用一张内存结构图来看看整个变化过程:

String为什么是不可变的?

其实上面的"ABC","123"是字符串常量,按照JVM规范应该是存放在方法区的常量池里面。但是Java1.7之后HotSpot虚拟机并没有区分方法区和堆,所以,这里统一就当做是放在堆里面的吧。

String源码构成

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; /** Cache the hash code for the string */
private int hash; // Default to 0

通过源码我们可以知道String底层是由char数组构成,我们创建一个字符串对象的时候,其实是将字符串保存在char数组中,因为数组是引用对象,为了防止数组可变,JDK加了final修饰,但是加了final修饰的数组只是代表了引用不可变,不代表数组内容不可变,因此JDK为了真正防止不可变,又加了private修饰符。

String对象是真的不可变吗?

从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中,value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗?我们来看下面的代码:

final int[] value={1,2,3}
int[] another={4,5,6};
value=another; //编译器报错,final不可变

value用final修饰,编译器不允许我把value指向堆区另一个地址。但如果我直接对数组元素动手,分分钟搞定

final int[] value={1,2,3};
value[2]=100; //这时候数组里已经是{1,2,100}

所以String是不可变,关键是因为设计源代码的工程师,在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。private final char value[]这一句里,private的私有访问权限的作用都比final大。而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final。

不可变有什么好处?

1、多线程下安全性

最简单地原因,就是为了安全。因为String是不可变的,因此多线程操作下,它是安全的,我们来看下面一段代码:

public String get(String str){
str += "aaa";
return str;
}

试想一下,如果String是可变的,那么get方法内部改变了str的值,方法外部str也会随之改变。

2、类加载中体现的安全性

类加载器要用到字符串,不可变提供了安全性,以便正确的类被加载,例如你想加载java.sql.Connection类,而这个值被改成了xxx.Connection,那么会对你的数据库造成不可知的破坏。

3、使用常量池可以节省空间

像下面这样字符串one和two都用字面量"something"赋值。它们其实都指向同一个内存地址

String one = "someString";
String two = "someString";

这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。

总结了这么多,面试答案就靠大家自己总结了,或者可以通过留言分享给他人哟。