String对象是不可变的,但引用变量是可变的。这是什么意思?

时间:2015-06-13 17:19:15

标签: java

我正在学习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

我在这里不明白的两点是:

  1. 当我取消注释s1 = s1 + "d"输出更改为false时。如果我用包装器Integerint替换String,也会发生同样的事情。
  2. 同样,当我将代码更改为使用StringBuffer时,这样:

    StringBuffer sb = new StringBuffer("a"); 
    StringBuffer sb2 = sb;
    //sb.append("c");
    System.out.println(sb == sb2);
    

    现在输出没有变化,即使我取消注释true语句,它仍然是sb.append

  3. 我无法理解这种奇怪的行为。有人可以解释一下。

2 个答案:

答案 0 :(得分:3)

s2是第一种情况下对s1的引用。在第二种情况下,+被翻译为s1.concat("d"),这会创建一个新字符串,因此引用s1s2指向不同的字符串对象。

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

答案 1 :(得分:2)

不可变场景

String类以及IntegerDouble等包装类都是不可变。这意味着当您执行以下操作时:

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

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

  1. (第1行)在内存地址"a"处创建字符串0x000001
  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行)比较s1s2的值,它们现在分别位于0x0000020x000001。显然,它们没有相同的值(内存地址),因此结果为false
  7. (第4行)将false打印到控制台。
  8. 因此,您看到,在将"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行)在内存地址StringBuffer处创建一个新的0x000001实例,内部状态为"a"
    2. (第1行)将sb的值设置为0x000001,以便它有效地指向StringBuffer实例,该实例本身包含"a"作为其状态的一部分。
    3. (第2行)复制sb的值并将其设置为sb2。因此,现在sbsb2都具有相同的0x000001值,因此两者都指向同一个StringBuffer实例。
    4. (第3行)查找sb指向的内容(StringBuffer实例),并在其上调用.append()方法,要求其从{{1}变更其状态到"a"。 (非常重要!!! 与不可变版本不同,"ab"的内存地址 NOT 更改。所以sb和{{1}仍然指向同一个sb实例。
    5. (第4行)比较sb2StringBuffer的值,它们都在sb。这次,它们都具有相同的值,因此结果为sb2
    6. (第4行)将0x000001打印到控制台。
    7. 奖金考虑因素:truetrue

      一旦你理解了上述内容,那么你现在拥有了所需的知识,可以更好地理解这种奇特的场景:

      ==

      令人惊讶的是,第3行返回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 运算符的比较结果,再加上对false等不可变类的更好理解,那么它实际上并不难理解,它教会了我们一个有价值的教训。

      因此,如果我们再次检查实际情况,我们会发现以下内容:

      1. (第1行)在内存地址==创建字符串String
      2. (第1行)将"abc"的值设置为0x000001,使其有效地指向字符串s1
      3. (第2行)在内存地址0x000001处创建一个新字符串"abc"。 (请注意,我们现在有2个字符串"abc"。一个位于内存地址0x000002,另一个位于"abc"。)
      4. (第2行)将0x000001的值设置为0x000002,以便它有效地指向第二个字符串s2
      5. (第3行)比较0x000002"abc"的值,它们现在分别位于s1s2。显然,它们没有相同的值(内存地址),因此结果为0x000001。 (即使它们都指向逻辑上相同的字符串,在内存中,它们仍然是2个不同的字符串!)
      6. (第3行)将0x000002打印到控制台。
      7. (第4行)在变量false指向的字符串(地址false)上调用.equals()。并且作为参数,传递对变量s1指向的字符串的引用(地址0x000001)。 s2方法比较两个字符串的值,并确定它们逻辑相等,因此返回0x000002
      8. (第4行)将equals打印到控制台。
      9. 希望以上情况对您有意义。

        还有教训吗?

        truetrue不同。

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

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

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

        我希望这会对你有所帮助。

相关问题