讨论Java中字符串的不变性

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语言实现的复杂性。

上一篇:Java 中的 CAS 简述及原理解析


下一篇:C#,是否可以将字节*转换为字节[]而无需复制?最快的方法是什么?