String
String不可变的理解与原因
在面向对象编程中,对象不可变指的是对象被创建后,其状态不可在变化。在Java中通过关键字final来实现变量的不可变。举个例子,比如:
String str = "A";
System.out.print(str); //A
str = "B";
System.out.print(str); //B
从以上来看似乎String类型是可变的(因为str的值变了啊),这样理解是错误的,我们从内剖析来看。在JVM中提到,对象的引用放在虚拟机栈中,对象放Java堆中(该部分有兴趣可以去了解)。
String str = "A"
在JVM中可以简单地分解为以下步骤:
- 在栈中创建一个String对象的引用str;
- 在String常量池中创建字符串“A”对象实例(字符串池中不存在“A”字符串,所以会创建实例,后面会解释);
- 对象引用str指向字符串池中“A"的对象实例。
str = "B"
的步骤为: - 在String常量池中创建字符串“B”对象实例
- 对象引用str指向字符串池中“B"的对象实例
从这我们可以清楚看到,str的引用指向了一个新的对象实例“B”内存地址,所以我们不能从这说明String的值发生变化了。
回到开头说的,对象不可变指创建对象后,其状态不可变。查看String源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; //字符数组
我们发现String的字符串是由字符数组char组成,所以假设我们要使String可变化,那就可以通过set方法,或者对象.属性改变其状态。但因为String对象的value属性是私有的,也没有暴露set方法,最主要的原因是因为value被关键字final所修饰。final关键字可修饰类,方法与变量(区别在此不谈)。
当final修饰变量时,该变量只能被赋值一次,赋值后不可在变,也就是说明,final修饰的变量必须要显示初始化,初始化后不可再变化,这就导致了String对象不可变的原因。
String两种创建方式的区别
我们经常使用的字符串创建方式是
String str = "A";
还有一种初始化创建方式是通过new String();
String newStr = new String("A");
两者是有区别的,我们举个例子:
String str1 = "A";
String str2 = "A“;
String str3 = new String("A");
System.out.print(str1 == str2); // true
System.out.print(str3== str1); //false
一步一步来理解。我们第一节讲过String常量池,字符串常量池是Java设计者为提高性能所设计的。当要实例化字符串时,都会在字符串常量池中判断是否存在该字符串。存在则返回字符串引用,不存在则实例化字符串放在字符串常量池中。
所以当执行String str1 = "A"
时,字符串常量池中不存在“A”,所以会创建该实例存储在字符串常量池中,当执行String str2 = “A"时,字符串常量池中已存在"A”,所以直接返回该实例。
所以两引用指向的都是同一对象,所以System.out.print(str1 == str2);
输出为true。
String str3 = new String("A");
通过new String(“XXX”)
方式实例化字符串有点特别,我们查看其构造方法源码:
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
我们查看该构造方法的注释说明,简单翻译就是该构造方法初始化String对象时,字符数组即value与参数的String对象即original的value的引用指向同一个字符数组,也可以说新创建的String是参数字符串的一个副本。
回到new String("A");
来看,这一操作分为两种情况:
- 字符串常量池中不存在"A"实例
在堆中创建一个String实例,并进行相应的初始化,然后执行该构造函数时,会在字符串常量池中实例化一个字符串"A",并将在字符串常量池中的对象"A"的value数组的引用给在堆中新创建的字符串的value,还有相同的hash值。此时可以说创建了两个对象,一个在字符串常量池中,一个在Java堆中。 - 字符串常量池中存在"A"实例
此过程与上面的过程差不多,只是不在字符串常量池中实例化"A"了,直接将已经存在的"A"的value字符数组引用给堆中新创建的String的value。所以只创建了一个对象,即堆中的String。
所以System.out.print(str3== str1);
输出为false,因为str3的实例在堆中,str1的实力在字符串常量池中,比较当然为false,因为不是同一个对象。
关于String hashCode的的补充
查看String的hashCode的方法的源码(JDK8):
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;
}
从源码可以看出,因为String重写了equals方法,String的hashCode也需要重写,hashcode的值是根据value所计算出来的,并且只计算一次(在调用hashCode方法的时候计算,否则hash值默认为0)。我们可以通过写例子打断点来看hash值是在调用hashCode方法进行计算后才赋值的:
从这也验证出上面所讲的,
堆中新建的字符串字符数组即value与参数的String对象即original的value的引用指向同一个字符数组,也可以说新创建的String是参数字符串的一个副本。
另外补充,不能通过判断hashCode的值相同而断定String的对象是同一个对象(可通过判断hashCode不同而断定不是同一个对象),也不能断定其字符数组引用指向同一对象,即String.equals的判断不能仅仅通过hashCode相等来进来判断字符串的值相等,因为会出现字符串不同,而所计算的hashCode的值是相同的情况。
String str1="Aa";
String str2="BB";
System.out.println(str1.hashCode()); //2112
System.out.println(str2.hashCode()); //2112
举一个例子反证法就能证明该命题的错误。