什么是不可变对象?
如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变
定义一个字符串
String s = "abcd";
s 中保存了 String 对象的引用。
使用变量来赋值变量
String s2 = s;
s2 保存了相同的引用值,因为他们代表同一个对象。
字符串连接
s = s.concat("ef");
s 中保存的是一个重新创建出来的 String 对象的引用。
String 总结
一旦一个 String 对象在内存(堆)中被创建出来,它就无法被修改(因为 String 类的所有成员变量都是 private,并且没有提供 public 的 set 方法来修改这些值。此外成员变量都是 final 的,这就意味着一旦初始化就无法修改)。特别要注意的是,String 类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象
如果需要一个可修改的字符串,应该使用 StringBuffer 或者 StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的 String 对象被创建出来
String 类不可变性的好处
只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么将不能实现 String interning(指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变
如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的
类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载 java.sql.Connection 类,而这个值被改成了 myhacked.Connection,那么会对你的数据库造成不可知的破坏
因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就使得字符串很适合作为 Map 中的键,字符串的处理速度要快过其它的键对象。这就是为什么 HashMap 中的键往往都使用字符串
String 对象真的不可变吗
JDK 6 String 源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
JDK 7 String 源码:
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
String 的成员变量是 private final 的,也就是初始化之后不可改变。但是在这几个成员中, value 比较特殊,因为他是一个引用变量,而不是真正的对象。value 是 final 修饰的,也就是说 final 不能再指向其他数组对象,那么能改变 value 指向的数组吗? 比如将数组中的某个位置上的字符变为下划线 “_”。至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个 value 引用,更不能通过这个引用去修改数组
那么用什么方式可以访问私有成员呢? 没错,用反射,可以得到 String 对象中的 value 属性, 进而改变通过获得的 value 引用改变数组的结构
public void testReflection() throws Exception {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//获取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); //Hello_World
}
结果:
s = Hello World
s = Hello_World
在这个过程中,s 始终引用的同一个 String 对象,但是在反射前后,这个 String 对象发生了变化,也就是说,通过反射是可以修改所谓的“不可变”对象的。但是一般我们不这么做。这个反射的实例还可以说明一个问题:如果一个对象,它组合的其他对象的状态是可以改变的,那么这个对象很可能不是不可变对象。例如一个 Car 对象,它组合了一个 Wheel 对象,虽然这个 Wheel 对象声明成了 private final 的,但是这个 Wheel 对象内部的状态可以改变, 那么就不能很好的保证 Car 对象不可变