字符串对象是不可变的,但引用变量是可变的.这是什么意思



我在学习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。如果我用包装器Integerint替换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"),因此引用s1s2指向不同的字符串对象。

    StringBuffer的情况下,参考永远不会改变。append改变了缓冲区的内部结构,而不是对它的引用

    不可变场景

    String类和包装器类(如IntegerDouble)都是不可变的。这意味着当你做一些类似的事情时:

    1. String s1 = "a";
    2. s2 = s1;
    3. s1 = s1 + "b";
    4. System.out.println(s1 == s2); // prints false
    

    注意真正发生的事情(非常简化,并使用伪造的内存地址):

    1. (第1行)在内存地址0x000001创建一个字符串"a"
    2. (第1行)将s1的值设置为0x000001,使其有效地指向字符串"a"
    3. (第2行)复制s1的值并将其设置为s2。所以现在s1s2都有相同的值0x000001,所以都指向字符串"a"
    4. (第3行)找到s1指向的内容(字符串"a"),并使用它创建一个新的、不同的"ab"字符串,该字符串将位于0x000002的不同内存地址。(注意,字符串"a"在存储器地址0x000001处保持不变)
    5. (第3行)现在将值0x000002分配给变量s1,以便它现在有效地指向这个新字符串"ab"
    6. (第4行)比较现在分别处于0x0000020x000001s1s2的值。很明显,它们没有相同的值(内存地址),所以结果是false
    7. (第4行)将false打印到控制台

    所以,当将"a"字符串更改为"ab"字符串时,您并没有修改"a"字符串。相反,您创建了一个新值为"ab"的第二个不同字符串,然后更改一个引用变量以指向这个新创建的字符串。

    当使用其他类(如IntegerDouble)进行编码时,也会出现完全相同的模式,这些类也是不可变的。您必须明白,当您在这些类的实例上使用+-之类的运算符时,您不会以任何方式修改实例。相反,您正在创建一个全新的对象,并获得对该新对象内存地址的新引用,然后可以将其分配给引用变量。

    可变场景

    这与可变类(如StringBufferStringBuilder)以及其他类(如不幸的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. (第1行)在内存地址0x000001创建一个内部状态为"a"的新StringBuffer实例
    2. (第1行)将sb的值设置为0x000001,使其有效地指向StringBuffer实例,该实例本身包含"a"作为其状态的一部分
    3. (第2行)复制sb的值并将其设置为sb2。因此,现在sbsb2都具有相同的0x000001值,因此都指向同一个StringBuffer实例
    4. (第3行)查找sb指向的对象(StringBuffer实例),并调用其上的.append()方法,要求其将状态从"a"更改为"ab"。(非常重要!!与不可变版本不同,sb的内存地址不会发生NOT更改。因此sbsb2仍然指向同一个StringBuffer实例
    5. (第4行)比较仍处于0x000001sbsb2的值。这一次,它们都有相同的值,所以结果是true
    6. (第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. (第1行)在内存地址0x000001处创建字符串"abc"
    2. (第1行)将s1的值设置为0x000001,使其有效地指向字符串"abc"
    3. (第2行)在存储器地址0x000002处创建新的字符串"abc"。(请注意,我们现在有两个字符串"abc"。一个位于内存地址0x000001,另一个位于0x000002)
    4. (第2行)将s2的值设置为0x000002,使其有效地指向第二个字符串"abc"
    5. (第3行)比较现在分别处于0x0000010x000002s1s2的值。很明显,它们没有相同的值(内存地址),所以结果是false。(尽管它们都指向逻辑上相同的字符串,但在内存中,它们仍然是两个不同的字符串!)
    6. (第3行)将false打印到控制台
    7. (第4行)在变量s1(地址0x000001)指向的字符串上调用.equals()。作为参数,传递对变量s2(地址0x000002)指向的字符串的引用。equals方法比较两个字符串的值,并确定它们在逻辑上相等,因此返回true
    8. (第4行)将true打印到控制台

    希望以上内容现在对您有意义。

    教训是什么?

    CCD_ 119与CCD_。

    ==将盲目地检查变量的值是否相同。在引用变量的情况下,这些值是内存地址位置。因此,即使2个变量指向逻辑上等价的对象,如果它们在内存中是不同的对象,也会返回false。

    equals()用于检查逻辑相等性。这意味着什么,具体取决于您调用的equals()方法的具体实现。但总的来说,这是一个直观地返回我们期望的结果的方法,也是您在比较字符串时想要使用的方法,以避免令人讨厌的意外惊喜。

    如果您需要更多信息,我建议您进一步搜索不可变类与可变类的主题。还有关于价值与参考变量的话题。

    我希望这对你有帮助。

    相关内容

    • 没有找到相关文章

    最新更新