为什么String类是不可变的?
为什么String类是不可变的?
#
String类
什么是
  当满⾜以下条件时,对象才是不可变的:
对象创建以后其状态就不能修改。
对象是正确创建的(在对象的创建期间,this引⽤没有逸出)。
  这是《Java并发编程实战》⼀书中的定义。在书中,说明并不是⼀定要将所有的域都设为final类型,⽐如String类就是这种情况,String 会将散列值的计算推迟到第⼀次调⽤hashCode()时进⾏,并将计算得到的散列值缓存到⾮final类型的域中,但这种⽅式之所以可⾏,是因为在每次计算时都得到相同的结果,这
基于⼀个不可变的状态(书中指出:⾃⼰编写代码时不要这么做,推荐全都设为final)。
  因此,不可变对象可以理解为:如果⼀个对象,在它正确创建完成之后,不能再改变它的状态(包括基本数据类型的值不能改变,引⽤类型的变量不能指向其他的对象,引⽤类型指向的对象的状态也不能改变),那么这个对象就是不可变的。
“不可变的对象”与“不可变的对象引⽤”区别
⽐如:
String str = "test";
str = "test1";
我们从下图可以看到,当定义String str = "test1"时,其实不是真正改变了str的内容,⽽是改变了str的引⽤。
  那么何为"不可变的对象引⽤"呢?final只保证引⽤类型变量所引⽤的地址不会改变,即⼀直引⽤同⼀个对象,但是这个对象的内容(对象的⾮final成员变量的值可以改变)完全可以发⽣改变(⽐如final int[] intArray;,intArray不允许再引⽤其他对象,但是intArray内的int值却可以被修改)。
为什么String对象是不可变的?
  要理解String的不可变性,⾸先看⼀下String类中都有哪些成员变量。在JDK1.8中,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
  其中,成员变量hash并没有⽤final声明,但是由于第⼀次调⽤hashCode()会重新计算hash值,并且以后调⽤会使⽤已缓存的值,当然最关键的是每次计算时都得到相同的结果,所以也保证了对象的不可变。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
  在Java中,数组也是对象,所以value也只是⼀个引⽤,它指向⼀个真正的数组对象。其实执⾏了String s = “ABCabc”; 这句代码之后,真正的内存布局应该是这样的:
  value是String封装的数组,value中的所有字符都是属于String这个对象的。由于value是private的,并且没有提供setValue等公共⽅法来修改这些值,所以在String类的外部⽆法修改String。也就是说⼀旦初始化就不能修改。此外,value变量是final的,也就是说在String类内部,⼀旦这个值初始化了,value引⽤类型变量所引⽤的地址不会改变,即⼀直引⽤同⼀个对象。所以可以说String对象是不可变对象。但其实value所引⽤对象的内容完全可以发⽣改变()。
如何理解substring, replace, replaceAll, toLowerCase等⽅法
⽐如:
String a = "ABCabc";
System.out.println("a = " + a);
a = a.replace('A', 'a');
System.out.println("a = " + a);
  a的值看似改变了,其实也是同样的误区。再次说明, a只是⼀个引⽤,不是真正的字符串对象,在调⽤a.replace('A', 'a')时,⽅法内部创建了⼀个新的String对象,并把这个新的对象重新赋给了引⽤a。String中replace⽅法的源码可以说明问题:
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
String类不可变性的好处
1. 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运⾏时节约很多heap空间,因为不同的字符串引⽤可以指
向池中的同⼀个字符串。但如果字符串是可变的,如果变量改变了它的值,那么其它指向这个值的变量的值也会⼀起改变。
2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的⽤户名、密码都是以字符串的形式传⼊数据库,以获得数据库
的连接,或者在socket编程中,主机名和端⼝都是以字符串的形式传⼊。因为字符串是不可变的,所以它的值是不可改变的,否则⿊客们可以钻到空⼦,改变字符串指向的对象的值,造成安全漏洞。
3. 因为字符串是不可变的,所以是多线程安全的,同⼀个字符串实例可以被多个线程共享。这样便不⽤因为线程安全问题⽽使⽤同步。
4. 类加载器要⽤到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,⽽这个值被改成了
myhacked.Connection,那么会对你的数据库造成不可知的破坏。
5. 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算,这就使得字符串很适合作为Map中的键,字符
串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使⽤字符串的原因。字符串常量不可改变

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。