Java中String对象被设计成是不可变的,这主要体现在下面方面:
1、class String被声明为final。
2、class String的char[]不可被访问。存在以char[]为参数的构造函数或者substring方法都是通过拷贝副本的方式实现的。
我们来研究一下这两个设计的目的,首先为什么class String被声明为final?
class String被声明为final本身代表着这个类的不可继承性,不可继承说明其没有任何子类,所以这样子就不存在子类重写class String破坏其不可变性的可能性。举个例子,如果我们实现了一个继承String实现了MutableString,这个类存在这样一个方法setCharAt(int index, char ch)
,当我们声明String mutableString = new MutableString("string")
时,这个mutableString可能会被当成一个普通字符串被到处引用,普通字符串都是满足不变性的,但是如果有个操作将mutableString向下转型,这时候就可以通过setCharAt(int index, char ch)
方法修改字符串,字符串不变性就被破坏了。
对于第二个设计,String的char[]不可被访问,我们可以通过unsafe修改value数组看看会有什么奇妙的事情。
public static void setStringCharAt(String str, int i, char ch)throws NoSuchFieldException, IllegalAccessException{
Unsafe unsafe = getUnsafe();
Class<?> stringClass = String.class;
Field value = stringClass.getDeclaredField("value");
value.setAccessible(true);
long valueAddress = unsafe.objectFieldOffset(value);
Class<?> charArrayClass = char[].class;
long baseOffset = unsafe.arrayBaseOffset(charArrayClass);
int scale = unsafe.arrayIndexScale(charArrayClass);
Object valueObject = value.get(str);
long charAddress = i * scale + baseOffset;
unsafe.putChar(valueObject, charAddress, ch);
}
public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe) theUnsafe.get(null);
}
public static void main(String[] args) throws Exception{
String str0 = "abc";
String str1 = "abc";
/**
* 操作虽多,只为了将字符串str0的第一个元素修改为'c'
*/
setStringCharAt(str0, 0, 'c');
System.out.println(str0 + str1);
}
这个程序通过声明了两个变量str0、str1,我们修改str0的第一字符,然后输出这两个字符串,有些人可能觉得这个结果是cbcabc,但事实并非如此。
我们首先需要注意,这两个变量赋值使用的是字符串字面量,对于字符串字面量,Java是有优化手段的–将两个变量指向同一个运行时常量池的引用,所以其实两个变量的value引用指向的都是常量池的引用,我们通过unsafe修改了一个字符串的value数组,受影响的可能不仅是当前变量,可能影响到了千千万万个。综上所述,结果很显然是cbccbc。
从这些方面讲,Java字符串的不变性是为了避免很多潜在的风险,也为了简化整个Java语言实现的复杂性。