talk is cheap, show me the code.
String a = "abcd";
a = "abcdef";
大多数人看了上面这两句代码,都认为a
由abcd
变成了abcdef
,而且a
是String
类型的,这句String不可变
不攻自破啊?那么真的是这样吗?
这个理解是错误的。大多数人对String不可变
这句话的理解都容易陷入上面这种思想。
而这两句代码的真正含义是:
首先将String
类型的变量a
赋值为abcd
,再将变量a
赋值为abcdef
。
进行第二次赋值时不是在原内存地址上进行修改数据,而是在堆中建了一个新的String
对象,并将栈中的引用指向了这个新对象,新地址。
所以abcd
这个字符串对象从创建出来后,始终都没有被改变。
翻开JDK源码,java.lang.String类起手前三行,是这样写的:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** String本质是个char数组. 而且用final关键字修饰.*/
private final char value[];
...
...
}
首先String
类是用final
关键字修饰,这说明String
不可继承。
再看下面,String
类的主力成员字段value
是个char[ ]
数组,而且是用final
修饰的。final
修饰的字段创建以后就不可改变。
有的人以为故事就这样完了,其实没有。
因为虽然value
是不可变,也只是value
这个引用地址不可变。
挡不住Array数组是可变的事实。
Array的数据结构看下图
也就是说value
只是在栈中存了这个数组的引用地址,数组的本体结构在堆。
同理,String
类里的value
用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}
或者更粗暴的反射直接改,也是可以的。如:
final int[] array={1,2,3};
Array.set(array,2,100); //数组也被改成{1,2,100}
所以String
是不可变,关键是因为SUN
公司的工程师,在后面所有String
的方法里很小心的没有去动Array
里的元素,没有暴露内部成员字段。
private final char value[]
这一句里,使用private
修饰value
,保证外部不可见;使用final
修饰value
,保证内部不改变value
的引用。
而且设计师还很小心地把整个String
设成final
禁止继承,避免被其他人继承后破坏。
所以String
是不可变的关键都在底层的实现,而不单单是一个final
的功劳。
考验的是工程师构造数据类型,封装数据的功力。
最简单的原因,就是为了安全。
String a, b, c;
a = "test";
b = a;
c = b;
a += "A";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();
b += "B";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();
c += "C";
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println();
控制台输出
testA
test
test
testA
testB
test
testA
testB
testC
从示例一
的输出可以看出String为不可变
的时候,b、c
是通过引用传递的方式进行赋值,
虽然一开始三个变量都指向了同一个地址,但是改变了a
的值,并没有影响后面b、c
的使用。
如果String是可变的,就可能如下例,我们使用StringBuffer
来模拟String
是可变的:
StringBuffera, b, c;
a = new StringBuffer("test");
b = a;
c = b;
a.append("A");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();
b.append("B");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();
c.append("C");
System.out.println(a.toString());
System.out.println(b.toString());
System.out.println(c.toString());
System.out.println();
控制台输出
testA
testA
testA
testAB
testAB
testAB
testABC
testABC
testABC
我么的本意是希望a、b、c
是不变的,是相互独立的,结果却并不是我们期望的那样。
所以String
不可变的安全性就体现在这里。
实际上StringBuffer
的作用就是起到了String
的可变配套类角色。
再看下面这个HashSet
用StringBuilder
做元素的场景,问题就更严重了,而且更隐蔽。
HashSet<StringBuilder> hs=new HashSet<>();
StringBuilder sb1 = new StringBuilder("aaa");
StringBuilder sb2 = new StringBuilder("aaabbb");
hs.add(sb1);
hs.add(sb2); // 这时候HashSet里是{"aaa","aaabbb"}
StringBuilder sb3 = sb1;
sb3.append("bbb"); // 这时候HashSet里是{"aaabbb","aaabbb"}
System.out.println(hs);
控制台输出
[aaabbb, aaabbb]
StringBuilder
型变量sb1
和sb2
分别指向了堆内的字面量aaa
和aaabbb
,并把它们插入到HashSet。
后面将sb1
赋值给sb3
,再改变sb3
的值,因为StringBuilder
没有不可变性的保护,
sb3
直接在原先aaa
的地址上改,导致sb1
的值也变了。
这时候,HashSet
上就出现了两个内容相等的字符串aaabbb
。破坏了HashSet
元素的唯一性。
所以千万不要用可变类型做HashMap
和HashSet
键
在并发场景下,多个线程同时对一个资源进行写操作,会出现线程安全的问题。
而不可变对象
不能被写,所以是线程安全的。
Q:Java中String类为什么要设计成final?
A:.安全性、效率
final
类型的类不能被继承,并且String
类中的final
方法可以防止其内部的方法被重写,乱改。final
类型的类被JVM
当作内联函数,提高了性能。在java中String类为什么要设计成final?