String的不可变性质
public class Test {
public static void main(String[] args) {
String str="1234";
changeStr(str);
System.out.println(str);
}
public void changeStr(String str) {
System.out.println(str);
str = "welcome"
System.out.println(str);
}
}
运行结果如下:
为什么是这么个结果呢,首先我们可以看看String类的源码。
从String源码我们可以看出String类实现了可序列化接口,比较器接口跟一个描述字符串的接口,并且用了final关键字修饰, 意味着这个类的方法是不能被继承的,然后我们再往下看,String类定义了一个private final修饰的char类型的数组变量,但是我们知道,被final修饰的基本类型是常量,但是被final修饰的引用类型的变量指向的地址值不能改变,但是实际上引用类型指向地址值的值是可以改变的,也就是说它里面的数据其实是可变的。
public class StringTest {
public static void main(String[] args) {
final StringBuffer str = new StringBuffer("hello");
//当我们用final修饰str时,赋值了一次,然后再把它指向新的对象他就会报下面这个错误,用final修饰的属性不能被赋值
//The final local variable str cannot be assigned. It must be blank and not using a compound assignment
//str = new StringBuffer("helloworld");
System.out.println(str);
//但是当我们像str添加值的时候,并没有报错,输出为helloworld
str.append("world");
System.out.println(str);
}
}
运行结果如下:
从这个结果我们可以得出final修饰的变量如果是引用类型的变量,实际上不可变的只是对地址值的指向时不可变的,但是final修饰的变量指向的地址值的值实际上是可以改变的。
因为String类的底层实际上是通过用final修饰类使之不可被继承,字符串的底层是用char类型的数组拼接起来,并用final修饰这个变量,但是如果这样的话,实际上String类是可以通过改变char数组的值来改变,因为数组实际上还是引用类型,依然可以通过改变值来使字符串发生改变,那么我们测试一下会不会发生改变
public class StringTest {
public static void main(String[] args) {
char[] c = new char[]{'1','h','r'};
String s = new String(c);
c[2] = '3';
System.out.println(s);
System.out.println(Arrays.toString(c));
}
}
运行结果如下:
上面代码运行结果可以看出来,即使改变了地址值的值String类型的值依旧话说没变,既然可以通过地址值的值改变来改变对象的值,为啥String类型却不可以呢,让我们来看看官方是怎么再java中来维护这种不可变性呢。让我们看看String类的构造方法源码。
当我们用substring,replace等方法的时候,String类中的处理的是会复制一份这个数组,然后把这份值存进String对象中,因此原数组的改变实际上是不会影响String对象的值的。实际上的处理是会new一个String对象出来,这个String变量实际上是指向新的内存地址,所以说在我们处理大量的String对象的时候,不建议使用String,而是使用Stringbuffer跟Stringbuild,每new一个对象就会分配内存,造成内存浪费。
但是为什么这第一行跟第三行输出的是同一个对象呢(因为String里重写了toString方法,所学甚浅,不会用其他方法得到对象的地址值…)这是因为String跟new String的区别,如果是new String的话,不管你常量池有没有这个常量,都会直接在栈区中开辟一块新的内存,把字符串的内容存进去,但是如果是String的话,会直接先指向常量池,首先判断常量池中是否有这个字符串对象,如果有就直接指向这个常量池中字符串对象的地址,如果没有才会选择new String,开辟一块新的内存空间,这种也算是官方对内存的优化吧…
具体可以看看这个例子:
public class StringTest {
public static void main(String[] args) {
String s = "hello";
final String s1 = "hello";
String ss = "hello";
String t1 = "world";
final String tt = "world";
String t2 = "helloworld";
String t3 = s+t1;
String t4 = s1+tt;
String t5 = "helloworld1";
final String t6 = "1";
String t7 = s1+tt+t6;
String sss = new String("hello");
String ssss = new String("hello");
System.out.println("s==ss的结果为"+(s==ss));
System.out.println("ss==sss的结果为"+(ss==sss));
System.out.println("sss==ssss的结果为"+(sss==ssss));
System.out.println("t2==t3的结果为"+(t2==t3));
System.out.println("t5==t7的结果为"+(t5==t7));
System.out.println("t4==t2的结果为"+(t4==t2));
}
}
运行结果如下:
当我们String直接双引号给值的时候,是会指向字符串常量池寻找有没有与它一样的对象,当我们new String的时候就会直接分配地址值,java中的==比较的是地址值,所以第一到三行的结果就是上图所示,但是第四五六行的结果为什么不同呢,这是因为,在java中用+拼接字符串同样是会找寻有没有这个值,如果常量池中有,就直接指向这个地址值,但是第四行的拼接值为啥反而是false,这是因为t3是由s和t1两个变量拼接而成,既然是变量,它的指向随时可能发生改变,这样的话t2就不一定等于t3了,就不会去常量池找了,而是直接开辟一块新的内存空间。而在五六行,t7跟t4两个变量全部由final修饰的两个变量拼接而成,final修饰的变量就不可能再改变,因此就会从字符串常量池中找,找不到才会去栈区开辟一块内存空间。
所以就可以总结最开始的那个问题了:
public class Test {
public static void main(String[] args) {
String str="1234";
changeStr(str);
System.out.println(str);
}
public void changeStr(String str) {
System.out.println(str);
str = "welcome"
System.out.println(str);
}
}
调用changeStr方法传参实际上复制了一份str(new String 将数据复制一份存到str里)传给chageStr方法,所以输出的自然是1234,然后在方法中把str指向了新的”welcome“,所以输出的是welcome,最后main方法中输出的str地址还是没变,指向的是最初的”1234“的地址值。