String不可变的分析
一.从代码层面分析
二.从内存结构角度分析
三.可以通过反射来修改char数组的值
四.一个例题
一.从代码层面分析:
string源码中真正存储元素的容器是final修饰的char数组,而且没有提供set方法来提供对它的修改,这是首先在代码层面上保证了不可变性。
而如StringBuilder和StringBuffer的char数组是提供了set方法,可以修改的。(真正的容器char数组是放在二者的父类AbstractStringBuilder中,而且它们的大部分方法都是使用的AbstractStringBuilder类的方法。)
String类中还有substring, replace, replaceAll, toLowerCase等方法可以获取改变后的字符串,它不是在源字符串的基础上改变的,而是在方法内部创建了一个新的String对象,赋值给引用。
例:
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
二.从内存结构角度分析:
String引用指向的对象的内容(对象内存地址所存的内容)是不能改变的,但String引用(变量)是可以改变的,可以让其指向另外一个字符串。(不能修改字符串的内容,但可以修改字符串的引用)
例:
String s1 = "hello";
s1 = "world";
这个代码中,开始s1指向的是堆中的一块内存地址内容是“hello”,执行到第二句代码时,s1引用重新指向了堆中新创建的另一块内存地址,内容是“world”。
所以String引用只要重新指向其他的字符串,那么除非新指向的这个字符串已经在字符串常量池中存在了,否则就会在堆中新创建一块内存空间。(jdk1.8中字符串常量池也是在堆中)
三.可以通过反射来修改char数组的值
那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:
String s1 = "a bc";
System.out.println(s1);// a bc
获取String类中的value字段
Field field = String.class.getDeclaredField("value");
//改变value属性的访问权限
field.setAccessible(true);
//获取s1对象上的value属性的值
char[] value = (char[]) field.get(s1);
//修改value中的值
value[1] = '_';
System.out.println(s1);// a_bc
在这个过程中,s始终引用的同一个String对象,但是在反射前后,这个String对象发生了变化, 也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。
四.一个例题
指出下列程序运行的结果:
public class Example{
String str=new String("tarena");
char[]ch={'a','b','c'};
public static void main(String args[]){
Example ex=new Example();
ex.change(ex.str,ex.ch);
System.out.print(ex.str+" and ");
System.out.print(ex.ch);
}
public void change(String str,char ch[]){
//引用类型变量,传递的是地址,属于引用传递。
str="test ok";
ch[0]='g';
}
}
结果是:tarena and gbc
分析:
因为是引用类型变量,所以传递的是地址,属于引用传递。即实参和形参开始是指向同一个内存地址的。
从前面对String类不可变特性的分析可知,这个题把str引用传递时,它在change方法内重新指向了一个新创建的内存地址。所以执行到这里时str引用和之前的已经不一样了,但这个因为是在方法内部的属于局部引用,会随着方法结束而消失,所以当这个方法执行完后,str又变成之前指向"tarena"字符串的引用。
输出的str不变也是由于String的不可变特性,这个char数组没有这个特性,它的实参和形参一直是指向同一个内存地址的,所以它会改变。
上述例子对StringBuilder的测试结果如下:
public class Example{
StringBuilder str = new StringBuilder("tarena");
char[]ch={'a','b','c'};
public static void main(String args[]){
Example ex=new Example();
ex.change(ex.str,ex.ch);
System.out.print(ex.str+" and ");
System.out.print(ex.ch);
}
public void change(StringBuilder str,char ch[]){
// StringBuilder没有str = "a";这种赋值操作
//str = new StringBuilder("hello");// 不变,因为是new新创建的,在堆中是新建一个内存空间
// 结果会改变,因为使用append是在原来的StringBuilder对象上改变的
str.append("_hello");
ch[0]='g';
}
}
参考博客如下:
- https://blog.csdn.net/weixin_43490440/article/details/101164669
- https://blog.csdn.net/zhangjg_blog/article/details/18319521