- String类是不可变类,类的实例创建的时候初始化所有的信息,并且这些信息不能被修改
- 字符串常量池:
是方法区中一块特殊的存储区域,当创建一个字符串常量的时候,判断该字符串字在符串字符串常量池中是否已经存在
如果存在,返回已经存在的字符串的引用;如果不存在,则创建一个新的字符串常量,并返回其引用
String s1 = "abcd"; String s2 = "abcd"; System.out.println(s1==s2) //true
变量s1,s2指向常量池中的同一个字符串常量对象;如果String是可变的,给一个变量重新赋值一个引用,将会指向错误的值(这一句没明白什么意思)
- 缓存哈希值:
在Java中字符串的哈希值会经常被使用到。例如在HashMap中,String的不可变总能保证哈希值总是相等的,并且缓存起来,不用担心会改变,
那意味着不需要每次都计算哈希值,这样会提高效率。在String类中有以下的代码:
//JDK1.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被作为参数广泛的在 Java 类中、网络连接中、和文件操作中使用,如果字符串可变,那么在这些模块中,将会引发严重的安全性问题boolean connect(string s){ if (!isSecure(s)) { throw new SecurityException(); } //here will cause problem, if s is changed before this by using other references. causeProblem(s); }
- 不可变的对象同样是线程安全的:
因为不可变的对象不能被改变,他们可以在多个线程中共享,就不需要使用线程的同步操作 总之,把String设计为不可变,是为了提高效率和安全性。在广泛的设计开发中,不可变类是首要选择。
什么是不可变对象?
区分对象和对象的引用
String s = "ABCabc";
System.out.println("s = " + s);
s = "123456";
System.out.println("s = " + s);
//输出
//s = ABCabc
//s = 123456
s的值确实改变了。那么怎么还说String对象是不可变的呢?
其实这里存在一个误区: s只是一个String对象的引用,并不是对象本身。对象在内存中是一块内存区,成员变量越多,这块内存区占的空间越大(String 对象存放在Method Area方法区)。引用只是一个4字节的数据,里面存放了它所指向的对象的地址,通过这个地址可以访问对象。
也就是说,s只是一个引用,它指向了一个具体的对象,当s=“123456”; 这句代码执行过之后,又创建了一个新的对象“123456”, 而引用s重新指向了这个心的对象,原来的对象“ABCabc”还在内存中存在,并没有改变。内存结构如下图所示:
为什么String对象是不可变的?
JDK1.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
在JDK7中,只有一个value变量,也就是value中的所有字符都是属于String这个对象的。这个改变不影响本文的讨论。 除此之外还有一个hash成员变量,是该String对象的哈希值的缓存,这个成员变量也和本文的讨论无关。在Java中,数组也是对象(可以参考我之前的文章java中数组的特性)。 所以value也只是一个引用,它指向一个真正的数组对象。其实执行了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:
value,变量都是private的,并且没有提供setValue等公共方法来修改这些值,所以在String类的外部无法修改String。
也就是说一旦初始化就不能修改, 并且在String类的外部不能访问这个成员。所以可以认为String对象是不可变的了。
那么在String中,明明存在一些方法,调用他们可以得到改变后的值。这些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代码:
String a = "ABCabc";
System.out.println("a = " + a);
a = a.replace('A', 'a');
System.out.println("a = " + a);
//结果:
//a = ABCabc
//a = aBCabc
那么a的值看似改变了,其实也是同样的误区。再次说明, a只是一个引用, 不是真正的字符串对象,在调用a.replace('A', 'a')时, 方法内部创建了一个新的String对象,并把这个心的对象重新赋给了引用a。String中replace方法的源码可以说明问题:
都是在方法内部重新创建新的String对象,并且返回这个新的对象,原来的对象是不会被改变的。这也是为什么像replace, substring,toLowerCase等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:
String ss = "123456";
System.out.println("ss = " + ss);
ss.replace('1', '0');
System.out.println("ss = " + ss);
//结果
//ss = 123456
//ss = 123456
String对象真的不可变吗?
从上文可知String的成员变量是private final 的,也就是初始化之后不可改变。那么在这几个成员中, value比较特殊,因为他是一个引用变量,而不是真正的对象。value是final修饰的,也就是说final不能再指向其他数组对象,那么我能改变value指向的数组吗? 比如将数组中的某个位置上的字符变为下划线“_”。 至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个value引用,更不能通过这个引用去修改数组。
那么用什么方式可以访问私有成员呢? 没错,用反射, 可以反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。下面是实例代码:
public static 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对象不可变。
参考: