(:这个专栏我会总结一下我感觉挺有意思的面试题知识点,巩固自己记忆,希望还能帮助到你们,
—————————————————————————————————
不可变对象可以理解为:
如果一个对象,在它正确创建完成之后,不能再改变它的状态(包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变),那么这个对象就是不可变的。
String str = "我是1";
str = "我是2";
也许有人会对上面的代码有疑惑,str不是改变了值了吗,String怎么会是不可变呢?
由于字符串存放在JVM的常量池中,而对象引用存放在JVM栈。所以,上述的代码只不过是str的引用由指向常量池中的“我是1”,变成了指向“我是2”。
什么是常量池?
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
因为常量池是编译期被确定,所以常常又会引出下面的问题。
//代码1
String s0=”hello”;
String s1=”hello”;
String s2=”hel” + “lo”;
System.out.println( s0==s1 );
System.out.println( s0==s2 );
结果为:
true
true
而
//代码2
String s0=”hello”;
String s1=new String(”hello”);
String s2=”hel” + new String(“lo”);
System.out.println( s0==s1 );
System.out.println( s0==s2 );
System.out.println( s1==s2 );
结果为:
false
false
为什么会出现这种情况呢?
代码1中s0和s1的”hello”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”hel”和”lo”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”hello”的一个引用。
而代码2中用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,堆内存中开辟一片新空间存放新对象。所以他们其实不是一个地址,为false。
说起new String() ,又有一道面试题是这样问的:
String str = new String("abc");
上面这条代码中,一共创建了几个对象?
答案是:两个或一个
如果代码如下,且aaa这个字符串之前没有用过,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象。
String str = new String(“aaa”);
如果代码如下,则只会在堆内存中开辟一片新空间存放新对象。
String str1 = "aaa";
String str = new String(“aaa”);
上面说了这么多,那么String为什么不可变?
在JDK1.8中,我们打开String的源码
映入眼帘的就是几个final修饰的类和变量。
之所以String不能被继承,是因为该类是final修饰的;
之所以String是不可变的,是因为char数组是final修饰的
虽然成员变量hash并没有用final声明,但是由于第一次调用hashCode()会重新计算hash值,并且以后调用会使用已缓存的值,当然最关键的是每次计算时都得到相同的结果,所以也保证了对象的不可变。
源码如下:
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
*
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
*
* using {@code int} arithmetic, where {@code s[i]} is the
* ith character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
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不是不可变吗,为什么我们可以调用String中的substring, replace, replaceAll, toLowerCase方法改变它?
String a = "ABCabc";
a = a.replace('A', 'a');
System.out.println("a = " + a);
我们需要明确的是,replace等方法并不能改变字符串本身的,只是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;
}
StringBuffer类弥补了String类的不足,StringBuffer类的内容可以修改。
String类不可变的好处有什么?
如有错误,敬请指出!
—————————————————————————————————
参考:
https://www.nowcoder.com/tutorial/94/b479698283ed4a36a0ff3e53e54794ff
https://zhidao.baidu.com/question/212316190.html
https://www.cnblogs.com/xzwblog/p/7230366.html