我本来以为我已经理解了String的不可变性,但没想到在写博客时发现我根本无法合理解释它的不可变性的原因,于是我又参考了韩顺平老师的Java教程中的解释才顺利写完。
String是一个引用数据类型,String类代表字符串。 Java程序中的所有字符串文字(例如"abc" )都被实现为此类的实例。
其实String代表字符串这个知识点很好理解,但对于Java中的String的不可变性,很多新手会感到一点疑惑。
就像这段代码
public static void main(String[] args) {
String str = "你好";
System.out.println(str);//输出你好
str = "不太好";
System.out.println(str);//输出不太好
}
明明同一个str开始是赋值为你好,之后赋值为不太好,在控制台也输出这两种结果,怎么能说字符串不可变呢?
一开始我真的很疑惑,因为我犯了一个低级错误,把它等同成了基本数据类型,其实当我们真正得将它看成一个引用数据类型,不可变性就会变得好理解了。
我们先把上面那段代码的认识误区给揪出来,我在代码中对同一个变量str 进行了两次赋值,但实际上是创建了两个不同的对象(String 是引用数据类型)。
在第一次赋值时,字符串常量 “你好” 会被存储在字符串池中。然后,变量str会引用该字符串常量(字符串常量这个在后面解释)的地址。
在第二次赋值时,由于字符串是不可变的,原有的 “你好” 字符串对象不会被修改,而是创建一个新的字符串常量 “不太好” 并存储在字符串池中。然后,变量str会引用新的字符串常量的地址。
字符串的不可变性意味着无法在现有的字符串对象上直接修改其内容。每次对字符串进行操作时,都会创建一个新的字符串对象来保存操作后的结果。
先解释一下在上文中提到的字符常量池:
字符串常量池是一块特殊的内存区域,用于存储所有字符串常量。它可以利用字符串的不可变性来共享字符串对象,提高内存的利用效率。
也就是说,当你使用字符串常量创建一个字符串对象时,比如“你好”,它将一直存在于字符串常量池中,无论你怎样操作或修改这个字符串对象。每当使用相同的字符串常量创建一个新的字符串对象时,实际上只是在引用同一个字符串常量,而不是在创建一个新的对象。
代码示例:
public static void main(String[] args) {
String str1 = new String("你好");
System.out.println(str1);//输出你好
String str2= new String("你好");
System.out.println(str2);//输出你好
}
调试结果:可以从调试结果看出,str1的value和str2的value是同一个496
以画图的方式类似于这样(注意:这里指的是最终引用,把中途的引用关系去掉了,是为了让大家更直观了解到不可变性)
正常情况下,当没有任何引用指向一个对象时,它成为了垃圾,可以被垃圾回收器回收。但是字符串常量池中的字符串对象有一个例外。由于字符串常量池的目的是为了提高字符串的共享和重用,JVM 对字符串常量池做了特殊的处理。
在Java中,字符串常量池中的字符串对象是在编译时或运行时被创建的,它们被认为是常量,通常是引用不变的。因此,即使没有直接的引用指向字符串常量池中的字符串对象,并且不会被垃圾回收器回收。当然也是有特殊情况能被回收的,不过这是在整个应用程序生命周期结束或者类被动态加载和卸载的特殊情况下才会发生。所以我们可以基本上看作字符串常量池中的字符串对象是一旦创建就一直存在的。
再用源码看一下String(Java8的源码)类的一些属性
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
...//省略其他方法
}
String类被final修饰,是最终类,不能被继承
它实现了三个接口:
它的重要成员变量有:
private final char value[]:value是一个是一个私有的char数组,用于存储字符。且被final修饰,它的值一旦初始化就无法改变了(这是String不变性的本质)。
private int hash:是用于缓存字符串的哈希码,默认为0。
==serialVersionUID ==:是用于序列化的版本号。
serialPersistentFields:是用于序列化的字段数组。
后面这两个属性不理解并不影响
我们再来看看String类型最常用的两种赋值方法:
String str1="你好";//直接赋值
String str2=new String("你好");//常用构造器赋值
System.out.println(str1==str2);
System.out.println(str1.equals(str2));
内存示意图(听说高版本的字符串常量池已经放在堆区了,而不是方法区)
这里解释一下:
str1的直接赋值是先看看字符串常量池里有没有“你好”这个字符串,没有的话,它会在字符串常量池里去创建一个“你好”的字符串对象。
str2的构造器赋值是先在堆中创建一个String对象,在前面我们已经提到过String类里有私有字符数组value这个属性了,之后会由这个数组指向字符串常量池里的字符串,如果字符串里有这个“你好”,数组就直接指向,没有则创建。
所以后面的比较你会发现第一个输出的是false,第二个是true
第一个==判断的是两者的对象地址是否相等,而第二个equals比较的是内容,我们可以看看String类的equals方法的源码。
public boolean equals(Object anObject) {
if (this == anObject) {//如果对象地址都相等,那内容肯定相同,后面就不需要比较了
return true;
}
if (anObject instanceof String) {//instanceof的作用是判断其左边对象是否为其右边类的实例
//返回boolean类型的数据,毕竟相同的数据类型比较才有意义
String anotherString = (String)anObject;//向下转换
int n = value.length;
if (n == anotherString.value.length) {//比较两个字符串对象的value长度
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {//判断两数组每一个对应的值都相等就返回true,
//有一个不相同就返回false
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
如果还没理解我们可以先试试几个案例,你可以自己判断一下输出的布尔值
public static void main(String[] args) {
String str1="你好";//直接赋值
String str2=new String("你好");//常用构造器赋值
String str3=str1+str2;//用变量名去拼接
String str4="你好"+"你好";//用字符去拼接,这里提示一下编译器会优化右边的,等价于String str4="你好你好";
System.out.println(str3==str4);
//这里解释一下intern方法是用于返回常量池里的字符串对象
//当调用intern方法时,如果池已经包含与equals(Object)方法确定的等于String对象的字符串,则返回来自池的字符串。
// 否则,此String对象将添加到池中,并返回对此String对象的引用。
//由此可见,对于任何两个字符串s和t,s.intern() == t.intern()为true当且仅当s.equals(t)为true 。
String str5=str1.intern()+str2.intern();
System.out.println(str3==str5);
System.out.println(str4==str5);
String str6=str5.intern();
System.out.println(str6==str4);
}
我先揭晓一下运行结果
这里的坑就是用变量名去拼接的话会调用StringBuilder创建一个新的对象,大家可以自己去调试调试,这里就不废话了,我直接用内存图表示一下应该就能理解了,红色表示直接指向字符串常量池,其他颜色都是间接指向
字符串不可变性确保字符串的内容无法被修改。这对于安全性是非常重要的,特别是在涉及到敏感信息、密码或网络通信等数据时。如果字符串是可变的,它们的值可以被改变,可能会导致潜在的安全漏洞。
由于字符串是不可变的,多个线程可以同时访问和共享字符串对象,而无需担心同步和数据一致性的问题。这简化了多线程编程的复杂性,并提高了程序的并发性能。
字符串的不可变性使得它们可以安全地用作哈希表的键,保证了哈希值的稳定性。如果字符串是可变的,当字符串的值发生改变时,它们的哈希值也会发生变化,破坏了哈希表的一致性。
字符串常量池是 Java 中的一个重要特性,它是用于存储字符串常量的一块内存区域。字符串的不可变性使得字符串常量池可以有效地利用字符串的共享,减少了内存占用。当创建一个新的字符串对象时,如果字符串常量池中已经存在相同内容的字符串,那么就会直接返回常量池中的引用,避免了重复创建相同内容的字符串对象。
字符串的不可变性使得编译器和运行时环境可以进行各种优化。例如,字符串拼接时可以使用 StringBuilder 的可变对象,避免了频繁的对象创建和拷贝。此外,JVM 也可以对不可变字符串进行缓存,提高代码的执行效率。
本来我想把方法一起写完的,但发现篇幅有点长了,就分我上下两篇吧