我在学习Kathy Sierra的Java书。我遇到了这样一个问题:
public class A {
public static void main(String args[]){
String s1 = "a";
String s2 = s1;
//s1=s1+"d";
System.out.println(s1==s2);
}
}
输出:true
我不明白的两点是:
当我取消注释s1 = s1 + "d"
时,输出更改为false
。如果我用包装器Integer
或int
替换String,也会发生同样的情况
同样,当我将代码更改为使用StringBuffer
时,如下所示:
StringBuffer sb = new StringBuffer("a");
StringBuffer sb2 = sb;
//sb.append("c");
System.out.println(sb == sb2);
现在输出没有改变,即即使我取消对sb.append
语句的注释,它仍然是true
。
我无法理解这种奇怪的行为。有人能解释一下吗?
s2
在第一种情况下是对s1
的引用。在第二种情况下,+
被转换为创建新字符串的s1.concat("d")
,因此引用s1
和s2
指向不同的字符串对象。
在StringBuffer
的情况下,参考永远不会改变。append
改变了缓冲区的内部结构,而不是对它的引用
不可变场景
String
类和包装器类(如Integer
和Double
)都是不可变的。这意味着当你做一些类似的事情时:
1. String s1 = "a";
2. s2 = s1;
3. s1 = s1 + "b";
4. System.out.println(s1 == s2); // prints false
注意真正发生的事情(非常简化,并使用伪造的内存地址):
- (第1行)在内存地址
0x000001
创建一个字符串"a"
- (第1行)将
s1
的值设置为0x000001
,使其有效地指向字符串"a"
- (第2行)复制
s1
的值并将其设置为s2
。所以现在s1
和s2
都有相同的值0x000001
,所以都指向字符串"a"
- (第3行)找到
s1
指向的内容(字符串"a"
),并使用它创建一个新的、不同的"ab"
字符串,该字符串将位于0x000002
的不同内存地址。(注意,字符串"a"
在存储器地址0x000001
处保持不变) - (第3行)现在将值
0x000002
分配给变量s1
,以便它现在有效地指向这个新字符串"ab"
- (第4行)比较现在分别处于
0x000002
和0x000001
的s1
和s2
的值。很明显,它们没有相同的值(内存地址),所以结果是false
- (第4行)将
false
打印到控制台
所以,当将"a"
字符串更改为"ab"
字符串时,您并没有修改"a"
字符串。相反,您创建了一个新值为"ab"
的第二个不同字符串,然后更改一个引用变量以指向这个新创建的字符串。
当使用其他类(如Integer
或Double
)进行编码时,也会出现完全相同的模式,这些类也是不可变的。您必须明白,当您在这些类的实例上使用+
或-
之类的运算符时,您不会以任何方式修改实例。相反,您正在创建一个全新的对象,并获得对该新对象内存地址的新引用,然后可以将其分配给引用变量。
可变场景
这与可变类(如StringBuffer
或StringBuilder
)以及其他类(如不幸的java.util.Date
)形成了鲜明对比。(顺便说一句,你最好养成使用StringBuilder
而不是StringBuffer
的习惯,除非你是为了满足多线程需求而故意使用它)
对于可变类,这些类的公开方法会改变(或变异)对象的内部状态,而不是创建一个全新的对象。因此,如果有多个变量指向同一可变对象,如果其中一个变量用于访问该对象并对其进行更改,则从任何其他变量访问同一对象也将查看更改。
因此,如果我们以这个代码为例(同样,请使用StringBuilder
,最终结果将是相同的):
1. StringBuffer sb = new StringBuffer("a");
2. StringBuffer sb2 = sb;
3. sb.append("b");
4. System.out.println(sb == sb2); // prints true
注意内部处理的不同(同样,非常简化,甚至省略了一些细节以保持简单易懂):
- (第1行)在内存地址
0x000001
创建一个内部状态为"a"
的新StringBuffer
实例 - (第1行)将
sb
的值设置为0x000001
,使其有效地指向StringBuffer
实例,该实例本身包含"a"
作为其状态的一部分 - (第2行)复制
sb
的值并将其设置为sb2
。因此,现在sb
和sb2
都具有相同的0x000001
值,因此都指向同一个StringBuffer
实例 - (第3行)查找
sb
指向的对象(StringBuffer
实例),并调用其上的.append()
方法,要求其将状态从"a"
更改为"ab"
。(非常重要!!与不可变版本不同,sb
的内存地址不会发生NOT更改。因此sb
和sb2
仍然指向同一个StringBuffer
实例 - (第4行)比较仍处于
0x000001
的sb
和sb2
的值。这一次,它们都有相同的值,所以结果是true
- (第4行)将
true
打印到控制台
奖金考虑:==
与equals()
一旦你理解了以上内容,那么你现在就具备了更好地理解这种特殊场景所需的知识:
1. String s1 = "abc";
2. String s2 = new String(s1);
3. System.out.println(s1 == s2); // prints false?!?
4. System.out.println(s1.equals(s2)); // prints true
令人惊讶的是,第3行返回false
(?!?)。然而,一旦我们理解了==
运算符所比较的内容,再加上对String
等不可变类的更好理解,实际上就不难理解了,它给我们上了宝贵的一课。
因此,如果我们再次检查真正发生的事情,我们会发现以下情况:
- (第1行)在内存地址
0x000001
处创建字符串"abc"
- (第1行)将
s1
的值设置为0x000001
,使其有效地指向字符串"abc"
- (第2行)在存储器地址
0x000002
处创建新的字符串"abc"
。(请注意,我们现在有两个字符串"abc"
。一个位于内存地址0x000001
,另一个位于0x000002
) - (第2行)将
s2
的值设置为0x000002
,使其有效地指向第二个字符串"abc"
- (第3行)比较现在分别处于
0x000001
和0x000002
的s1
和s2
的值。很明显,它们没有相同的值(内存地址),所以结果是false
。(尽管它们都指向逻辑上相同的字符串,但在内存中,它们仍然是两个不同的字符串!) - (第3行)将
false
打印到控制台 - (第4行)在变量
s1
(地址0x000001
)指向的字符串上调用.equals()
。作为参数,传递对变量s2
(地址0x000002
)指向的字符串的引用。equals
方法比较两个字符串的值,并确定它们在逻辑上相等,因此返回true
- (第4行)将
true
打印到控制台
希望以上内容现在对您有意义。
教训是什么?
CCD_ 119与CCD_。
==
将盲目地检查变量的值是否相同。在引用变量的情况下,这些值是内存地址位置。因此,即使2个变量指向逻辑上等价的对象,如果它们在内存中是不同的对象,也会返回false。
equals()
用于检查逻辑相等性。这意味着什么,具体取决于您调用的equals()
方法的具体实现。但总的来说,这是一个直观地返回我们期望的结果的方法,也是您在比较字符串时想要使用的方法,以避免令人讨厌的意外惊喜。
如果您需要更多信息,我建议您进一步搜索不可变类与可变类的主题。还有关于价值与参考变量的话题。
我希望这对你有帮助。