75

我了解到,当您在 Java 中修改变量时,它不会更改它所基于的变量

int a = new Integer(5);
int b = a;
b = b + b;
System.out.println(a); // 5 as expected
System.out.println(b); // 10 as expected

我假设对象也有类似的情况。考虑这个类。

public class SomeObject {
    public String text;

    public SomeObject(String text) {
        this.setText(text);
    }

    public String getText() {
        return text;
    }   

    public void setText(String text) {
        this.text = text;
    }
}

尝试此代码后,我感到困惑。

SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
s2.setText("second");
System.out.println(s1.getText()); // second as UNexpected
System.out.println(s2.getText()); // second as expected

请向我解释为什么更改任何对象都会影响另一个对象。我知道变量文本的值存储在两个对象的内存中的相同位置。

为什么变量的值是独立的但与对象相关?

另外,如果简单的分配不能完成这项工作,如何复制 SomeObject?

4

17 回答 17

136

Java 中的每个变量都是一个引用。所以当你这样做时

SomeClass s2 = s1;

您只需指向与指向s2相同的对象s1。您实际上是在将引用 s1(指向 的实例SomeClass)的值分配给 s2。如果你修改s1,s2也会被修改(因为它指向同一个对象)。

有一个例外,原始类型:int, double, float, boolean, char, byte, short, long. 它们按价值存储。所以在使用时=,你只赋值,但它们不能指向同一个对象(因为它们不是引用)。这意味着

int b = a;

仅将 的值设置b为 的值a如果你改变了ab就不会改变。

归根结底,一切都是按值赋值的,它只是引用的值,而不是对象的值(上面提到的原始类型除外)。

所以在你的情况下,如果你想复制s1,你可以这样做:

SomeClass s1 = new SomeClass("first");
SomeClass s2 = new SomeClass(s1.getText());

或者,您可以添加一个复制构造函数SomeClass,将实例作为参数并将其复制到自己的实例中。

class SomeClass {
  private String text;
  // all your fields and methods go here

  public SomeClass(SomeClass copyInstance) {
    this.text = new String(copyInstance.text);
  }
}

有了这个,你可以很容易地复制一个对象:

SomeClass s2 = new SomeClass(s1);
于 2012-08-22T12:08:27.327 回答
43

@brimborium 的回答非常好(他+1),但我只想用一些数字来详细说明。让我们首先进行原始分配:

int a = new Integer(5);
int b = a;
b = b + b;
System.out.println(a); // 5 as expected
System.out.println(b); // 10 as expected
int a = new Integer(5);

1- 第一条语句创建一个值为 5 的 Integer 对象。然后,在将其分配给变量a时,Integer 对象将被拆箱并a作为原语存储。

创建 Integer 对象后,赋值前:

在此处输入图像描述

分配后:

在此处输入图像描述

int b = a;

2-这只会读取 的值,a然后将其存储到b.

(Integer 对象现在可以进行垃圾收集,但此时还不一定是垃圾收集)

在此处输入图像描述

b = b + b;

3- 这会读取b两次的值,将它们加在一起,然后将新值放入b.

在此处输入图像描述


另一方面:

SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
s2.setText("second");
System.out.println(s1.getText()); // second as UNexpected
System.out.println(s2.getText()); // second as expected
SomeObject s1 = new SomeObject("first");

1- 创建一个新的SomeObject类实例,并将其分配给引用s1

在此处输入图像描述

SomeObject s2 = s1;

2-这将使引用s2指向指向的对象s1

在此处输入图像描述

s2.setText("second");

3-当您在引用上使用设置器时,它将修改引用指向的对象。

在此处输入图像描述

System.out.println(s1.getText());
System.out.println(s2.getText());

4- 两者都应该打印second,因为两个引用s1s2指的是同一个对象(如上图所示)。

于 2012-09-16T03:22:26.103 回答
16

当你这样做时

SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;

您有 2 个对同一对象的引用。这意味着无论您使用哪个引用对象,您所做的更改都将在使用第二个引用时可见。

可以这样想:房间里有一台电视,但有两个遥控器:不管你使用哪个遥控器,你仍然会对同一个底层对象(电视)进行更改。

于 2012-08-22T12:07:33.810 回答
10

当你写:

SomeObject s1 = new SomeObject("first");

s1不是一个SomeObject。它是对对象的引用。SomeObject

因此,如果您将 s2 分配给 s1,您只需分配引用/句柄,并且基础对象是相同的。这在 Java 中很重要。一切都是按值传递的,但你永远不会传递对象——只有对对象的引用。

因此,当您分配s1 = s2,然后调用s2更改对象的方法时,底层对象会更改,并且当您通过s1.

这是对象不变性的一个论据。通过使对象不可变,它们不会在您的控制下发生变化,从而以更可预测的方式表现。如果要复制对象,最简单/最实用的方法是编写一个copy()简单地创建新版本并复制字段的方法。您可以使用序列化/反射等进行巧妙的复制,但这显然更复杂。

于 2012-08-22T12:07:39.093 回答
4

Java程序员犯的十大错误

6 - 对按值传递和按引用传递的困惑

这可能是一个令人沮丧的诊断问题,因为当您查看代码时,您可能确定它是通过引用传递的,但发现它实际上是通过值传递的。Java 两者都使用,因此您需要了解何时通过值传递,何时通过引用传递。

当您将原始数据类型(例如 char、int、float 或 double)传递给函数时,您就是按值传递。这意味着数据类型的副本被复制,并传递给函数。如果函数选择修改该值,它将仅修改副本。一旦函数完成,控制权返回给返回函数,“真实”变量将保持不变,并且不会保存任何更改。如果您需要修改原始数据类型,请将其作为函数的返回值,或将其包装在对象中。

因为int是原始类型,int b = a;是按值复制,这​​意味着ab是两个不同的对象,但具有相同的值。

SomeObject s2 = s1;makes1s2同一个对象的两个引用,所以如果你修改一个,另一个也会被修改。

一个好的解决方案是实现另一个这样的构造函数:

public class SomeObject{

    public SomeObject(SomeObject someObject) {
        setText(someObject.getText());
    }

    // your code

}

然后,像这样使用它:

SomeObject s2 = new SomeObject(s1);
于 2012-08-22T12:12:08.257 回答
2

让我们从你的第二个例子开始:

第一条语句将一个新对象分配给s1

SomeObject s1 = new SomeObject("first");

当您在第二条语句 ( SomeObject s2 = s1) 中进行赋值时,您是在告诉指向当前指向s2的同一个对象,因此您有两个对同一个对象的引用。s1

请注意,您没有复制SomeObject,而是两个变量只是指向同一个对象。因此,如果您是 modifys1s2,您实际上是在修改同一个对象(请注意,如果您做了类似s2 = new SomeObject("second")它们现在指向不同对象的操作)。

在您的第一个示例中,a并且b是原始值,因此修改一个不会影响另一个。

在 Java 的底层,所有对象都使用按值传递来工作。对于对象,您传递的“值”是对象在内存中的位置(因此它似乎具有通过引用传递的类似效果)。基元的行为不同,只是传递值的副本。

于 2012-08-22T12:10:55.517 回答
2

上面的答案解释了您所看到的行为。

在回答“另外,如果简单的分配不能完成这项工作,如何复制 SomeObject?” - 尝试搜索cloneable(它是一个 java 接口,提供了一种复制对象的方法)和“ copy constructors”(另一种方法,可以说是更好的方法)

于 2012-08-22T12:11:04.860 回答
2

在您的代码中s1s2同一个对象(您只使用创建了一个对象new)并且您让s2指向下一行中的同一个对象。因此,当您更改它时,如果您通过和text引用该值,它都会发生变化。s1s2

Integers 上的+运算符创建一个对象,它不会更改现有对象(因此添加 5+5 不会给 5 新值 10...)。

于 2012-08-22T12:08:31.030 回答
2

这是因为 JVM 存储了一个指向s1. 当您调用 时s2 = s1,您基本上是在说s2指针(即内存地址)与 for 的值相同s1。由于它们都指向内存中的同一位置,因此它们代表完全相同的事物。

=运算符分配指针值。它不复制对象。

克隆对象本身就是一件复杂的事情。每个对象都有一个您可能很想使用的 clone() 方法,但它会进行浅拷贝(这基本上意味着它会复制顶级对象,但其中包含的任何对象都不会被克隆)。如果您想玩弄对象副本,请务必阅读 Joshua Bloch 的Effective Java

于 2012-08-22T12:09:32.090 回答
2

对引用的对象分配不会克隆您的对象。引用就像指针。它们指向一个对象,当调用操作时,它是在指针指向的对象上完成的。在您的示例中, s1 和 s2 指向同一个对象,setter 更改同一个对象的状态,并且更改在引用中可见。

于 2012-08-22T12:12:41.517 回答
2
int a = new Integer(5) 

在上述情况下,创建了一个新的 Integer。这里 Integer 是一种非原始类型,其中的值被转换(转换为 int)并分配给 int 'a'。

SomeObject s1 = new SomeObject("first");  
SomeObject s2 = s1;

在这种情况下,s1 和 s2 都是引用类型。它们不是为了包含像原始类型这样的值而创建的,而是包含某个对象的引用。为了便于理解,我们可以将引用视为一个链接,它指示我在哪里可以找到被引用的对象。

这里引用 s1 告诉我们在哪里可以找到“first”的值(它实际上存储在计算机内存中的 SomeObject 实例中)。换句话说,s1 是类“SomeObject”的对象的地址。通过以下分配 -

SomeObject s2 = s1;

我们只是将存储在 s1 中的值复制到 s2,我们现在知道 s1 包含字符串“first”的地址。在此分配之后,两个 println() 都会产生相同的输出,因为 s1 和 s2 都引用同一个对象。

如果您是 java 用户,则可以使用复制构造函数和 clone() 方法复制对象。可以通过以下方式使用克隆 -

SomeObject s3 = s1.clone(); 

有关 clone() 的更多信息,这是一个有用的链接http://en.wikipedia.org/wiki/Clone_%28Java_method%29

于 2012-09-16T17:11:17.133 回答
2

更改您的课程以创建新的参考而不是使用相同的参考:

public class SomeObject{

    public String text;

    public SomeObject(String text){
        this.setText(text);
    }

    public String getText(){
        return text;
    }   

    public void setText(String text){
        this.text = new String(text);
    }
}

你可以使用这样的东西(我不假装有理想的解决方案):

public class SomeObject{

    private String text;

    public SomeObject(String text){
        this.text = text;
    }

    public SomeObject(SomeObject object) {
        this.text = new String(object.getText());
    }

    public String getText(){
        return text;
    }   

    public void setText(String text){
        this.text = text;
    }
}

用法:

SomeObject s1 = new SomeObject("first");
SomeObject s2 = new SomeObject(s1);
s2.setText("second");
System.out.println(s1.getText()); // first
System.out.println(s2.getText()); // second
于 2012-08-22T12:15:10.257 回答
1

由于对象被引用引用。所以如果你写 s2=s1 只有引用会被复制。就像在 C 中一样,您只处理内存地址。如果您复制内存地址并更改此地址后面的值,则两个指针(引用)都将更改内存中此时的一个值。

于 2012-08-22T12:11:27.790 回答
1

第二行 ( SomeObject s2 = s1;) 只是将第二个变量分配给第一个变量。这导致第二个变量指向与第一个相同的对象实例。

于 2012-08-22T12:07:23.910 回答
1

那是因为s1并且s2仅作为对您的对象的引用。分配时s2 = s1您只分配引用,这意味着两者都将指向内存中的同一个对象(具有当前文本“first”的对象)。

当您现在在 s1 或 s2 上执行 setter 时,两者都将修改同一个对象。

于 2012-08-22T12:08:32.457 回答
1

当您将对象分配给变量时,您实际上是在将引用分配给该对象。所以这两个变量s1s2指的是同一个对象。

于 2012-08-22T12:08:39.737 回答
0
T copyDeep(T input) { 
    return new Gson().fromJson(new Gson().toJson(input), T.class);
}
于 2021-09-16T21:15:12.620 回答