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这个字符串对象从创建出来后,始终都没有被改变。
String为什么不可变?
翻开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的功劳。
考验的是工程师构造数据类型,封装数据的功力。
不可变有什么好处?
最简单的原因,就是为了安全。
示例1
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的可变配套类角色。
示例2
再看下面这个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类的不可变性原理,解释了为何String对象一旦创建就不能更改,以及这种设计如何保障代码的安全性和提高多线程环境下的效率。
2380

被折叠的 条评论
为什么被折叠?



