【问题标题】:String object is immutable but reference variable is mutable. What does that mean?字符串对象是不可变的,但引用变量是可变的。这意味着什么?
【发布时间】:2015-06-13 17:19:14
【问题描述】:

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

我无法理解这种奇怪的行为。谁能给我解释一下。

【问题讨论】:

  • == 测试对象身份。 s1 = s1 + "d" 创建一个新对象。但是sb.append() 不只是在sb 上调用一个方法。
  • 开始区分变量、引用和对象。
  • @markspace 如果== 用于检查相同的内存引用,那么如果我将String 更改为包装类DoubleInteger,为什么会得到输出false,如下所示:[这个][1][1]:docs.google.com/document/d/…
  • @JSK 出于同样的原因 s1 = s1 + "d" 打印错误。 它创建了一个新对象。

标签: java


【解决方案1】:

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

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

【讨论】:

  • "在第二种情况下,+ 被翻译成 s1.concat("d") 来创建一个新字符串" 我想如果你看一下字节码,你会找到它是StringBuilder sb = new StringBuilder(); sb.append(s1); sb.append("d"); s1 = sb.toString(); 但它涉及到同样的事情:创建了一个新字符串。
  • 小细节,但在第一种情况下,s2 并不是对s1 的真正引用。相反,它们都是对字符串 "a" 的引用。
  • @sstan:非常重要的区别。
  • @sstan 如果== 用于检查相同的内存引用,那么如果我将String 更改为包装类DoubleInteger,为什么会得到输出false,如下所示:[这个][1][1]:docs.google.com/document/d/…
  • @JSK:我添加了一个答案,希望能帮助您更好地理解这一点。如果还有什么不清楚的地方,请在我的回答下添加评论。
【解决方案2】:

不可变场景

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

奖金考虑:== vs. 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"。 (请注意,我们现在有 2 个字符串 "abc"。一个在内存地址 0x000001,另一个在 0x000002)。
  4. (第 2 行)将s2 的值设置为0x000002,使其有效地指向第二个字符串"abc"
  5. (第 3 行)比较 s1s2 的值,它们现在分别位于 0x0000010x000002。显然,它们没有相同的值(内存地址),所以结果是false。 (即使它们都指向逻辑上相同的字符串,但在内存中,它们仍然是 2 个不同的字符串!)
  6. (第 3 行)将false 打印到控制台。
  7. (第 4 行)在变量s1(地址0x000001)指向的字符串上调用.equals()。并作为参数传递对变量s2(地址0x000002)指向的字符串的引用。 equals 方法比较两个字符串的值,并确定它们在逻辑上相等,因此它返回true
  8. (第 4 行)将true 打印到控制台。

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

课程呢?

==equals() 不同。

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

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

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

希望对你有帮助。

【讨论】:

    猜你喜欢
    • 2015-06-18
    • 2012-10-08
    • 1970-01-01
    • 1970-01-01
    • 2017-07-24
    • 2010-09-18
    • 2023-02-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多