前几天,有个实习生问我,String类为什么被设计成不可变的,说他看见网上的都说可变。我看了看他,说:那篇博客你没看完吧!他挠了挠头回答说:没有!我又问他:知道string类有个变量value么?他说知道,我又问这个变量有哪些修饰符修饰的,他又挠了挠头,笑着说:忘了,好像有个private。我半开玩笑的笑着说:可以啊,这都能记着!然后让他先把bug解决了晚上在告诉他!有的小伙伴就要问了,为什么不直接告诉他还有个final呢?并且这个value变量还是个char数组呢?其实不是不想直接告诉他,而是他还是个实习生,不想让他丧失独自解决问题的能力和独自学习的能力,旁敲侧击应该会更好一些!好了,不扯了,进入正题了!
正如标题所说:String类为什么被设计成不可变?真的不可变么?首先回答第一个问号:String类为什么被设计成不可变?其实这个问题很简单,最重要的一点就是安全,当然还要考虑到综合原因,比如内存,数据同步,数据结构,设计考虑,效率优化问题等。看源码:
1public final class String
2 implements java.io.Serializable, Comparable, CharSequence {
3 /** The value is used for character storage. */
4 private final char value[];
5
6 /** Cache the hash code for the string */
7 private int hash; // Default to 0
8
9 ...
源码第四行可以看到,这个value变量是char数组类型的,也就是说,字符串的值都保存在这个数组中的。再接着看,有个关键字final。为什么呢?很简单,因为数组是引用了对象,为了保证数组不可变而加了个final。那么,问题又来了,final只是保证引用的地址不可变,值还是可变的呀!(LZ此时会心一笑,想到了小伙伴们肯定要用反射这个骚操作来来迫不及待的证明了)我们知道,一个类要被设计成不可变类,要遵循以下几条规则:
1.其状态被创建后不能再被修改
2.所有域都应该是 private final的(但只做到这一步还不够,因为如果成员变量是对象,它保存的只是引用,有可能在外部改变其引用指向的值)
3.其构造函数构造对象期间,this引用没有泄露。
4.只提供成员变量的get方法,不能提供set方法(避免通过其他接口改变成员变量的值,破坏不可变特性。)
5.如果类中包含可变类对象,那么返回给客户端的时候,返回该对象的一个深拷贝,而不是该对象本身
说到这里,再来看string类源码:
public final class String
implements java.io.Serializable, Comparable, 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
....
**
* Allocates a new {@code String} so that it represents the sequence of
* characters currently contained in the character array argument. The
* contents of the character array are copied; subsequent modification of
* the character array does not affect the newly created string.
*
* @param value
* The initial value of the string
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
......
**
* Converts this string to a new character array.
*
* @return a newly allocated character array whose length is the length
* of this string and whose contents are initialized to contain
* the character sequence represented by this string.
*/
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
如上源码所示,可以观察到String类的设计符合上面总结的不变类型的设计原则。其次,String对象的不可变是由于对String类型的所有改变内部存储结构的操作都会new出一个新的String对象。
一.不可变的好处:
1.安全:文章开头我们说了,被设计成不可变的最重要的原因就是安全,这也是好处。
多线程角度看,如果俩个现场同时读取一个字符串,其中一个线程修改了值,如果字符串可变,那么另一个线程必然读取的数据和之前不同,试想一下,这是一件多么可怕的事情!
类加载角度看,假入你想加载Java.util.*,而加载过程中被修改成了java.P*,哇,这下你写的代码全乱套了,数据库更是乱成了一锅粥,或爆出各种破坏性的错误,头大哟!
2.hashcode缓存:由于字符串的不可变,在创建对象时,hashcode就被缓存了,不需要重新计算。这也是Map喜欢将字符串作为键的原因,因为都缓存了,快啊,比其他对象作为键都要快!
3.节省字符串常量池的空间:(字符串常量池这个概念后续文章会详细解析)因为字符串不可变性,会有多个引用指向常量池中的同一个地址,节省了堆的空间,如果字符串可变,那么堆就要开辟N多空间来保存字符串,累啊,还浪费空间!
二.缺点:缺点就是由于不可重用性,会使得它们被用玩就仍,垃圾回收器很头大的,会产生很多垃圾!
好了,第一个问号解释完了,该第二个问号了。
第二个问号很好解答,上文中说过了呀:反射。看代码:
String s = "Hello World"; //创建字符串"Hello World", 并赋给引用s
System.out.println("s = " + s);
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true); //改变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的值变了吧,从始至终s的引用都指向一个地址,值却被改变了,哈哈哈!(这个骚操作其实是LZcopy的,LZ实在不想写了,困呀,上下眼皮都在打架了。但LZ不建议这么干!这样会破坏不可变特性的哟)