我们都知道String一旦定义后长度和内容是不可变的,那么我们要想知道String为什么是不可变的,需要从以下几个角度来看:
1.从源代码角度来看
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
在定义字符串时,会将字符串内容保存到一个使用 private final修饰的char[ ]之中,从源码中我们可以得知该数组被final修饰,因此它的引用地址不能改变。
但是这并不代表char value[ ]数组中的内容不可变,我们依旧可以通过数组下标来修改value数组,从此看来在源代码中String不可变的主要原因是存放数据的char[ ]数组被private修饰,我们从外部无法去访问到char[ ],并且String内部本身也没有向我们提供修改char value[ ]的API, 从而无法对字符串内容进行修改。
public class Test {
private final static char[] value = {'a','b','c'};
public static void main(String[] args) {
value[0] = 'A';//尽管value数组被final修饰,但我们依然可以通过数组下标对value数组进行内容的修改
}
}
2、从缓存池角度来看
String缓存池
String Pool 是在方法区的一块特殊存储区域。当一个String被创建时如果发现当前String已经存在于String Pool,则会返回一个已存在String的引用而不会新建一个对象。
以下代码只会创建一个String对象在堆内存中。
String s1 = "hello";
String s2 = "hello";
在缓存池中,如果一个String是可变的,改变了一个引用指向的String,那么就会导致其他引用得到错误的值。因此String不可被改变。
3.从使用频率来看
在Java中,对于String的Hashcode使用是非常频繁的,例如在HashMap或HashSet中。将String设计成不可变可以保证他的Hashcode始终一致,这样Hashcode就可以被缓存并且不用担心变化。这就意味着,不需要在每次使用String的时候都去计算他的Hashcode,这也使得程序运行的更加高效。
4.从安全性能来看
String在很多Java类中被广泛用作参数,但是我们知道不可变对象是天生线程安全的,因为不可变对象不能被改变,他们可以在多线程中被自由的共享,这就消除了对象同步的需求。那么从这个层面来说,String被设计成不可变的出发点是效率和安全。这也是不可变类在很多情况下被优先使用的原因。
那么String对象真的不可变吗?
上面我们提过char value[ ]数组是使用private final修饰的,但是这并不代表char value[ ]数组中的内容不可变,我们依旧可以通过数组下标来修改value数组,这只是从String内部进行修改,那么我们如何在外部去修改value[ ]的值呢?
我们可以通过反射来进行修改, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变字符串的内容。下面是实例代码:
public static void updateReflection() throws Exception {
//创建字符串
String s = "Hello World";
System.out.println("s = " + s);
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '.';
System.out.println("s = " + s);
}
在此过程中,我们通过反射的技术获取了String类中的value数组,通过数组下标修改了其内容,但字符串s的引用一直未发生改变。