用Java复制对象


74

我了解到,当您在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?


23
欢迎使用Java的参考类型 docstore.mik.ua/orelly/java-ent/jnut/ch02_10.htm
维克多

5
欢迎来到stackoverflow。这些是Java的基础。.您应该首先讲解Java概念..使用本教程..
pratikabu 2012年

2
我认为该示例令人困惑,即使选择的解决方案是正确的:Integer是anObject而不是原始类型。如果您编写Integer b = a;,则b引用与相同的实例a。但是在编写b = b+b;b时,它是指+操作员创建的新实例。
文斯(Vince)2012年

它是题外话,但不要在构造函数stackoverflow.com/questions/3404301/…中调用可重写的方法。
halex 2012年

另外,由于要提供使用它的getter和setter方法,因此您可能想声明String文本属性为私有,因此您隐藏了实现细节,并为类提供了更好的抽象(通常来说,是更好的OO实践)
stoldark

Answers:


134

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

SomeClass s2 = s1;

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

基本类型有一个例外: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);

13
+1引用和基元按值复制。出现了混淆,您无法在引用和引用的对象之间进行区分。;)
彼得·劳瑞

15
s1.clone()有时一个选项。
瑞安·阿莫斯

3
您可以使用s1.clone()将对象复制到s2,它将不会传递对s2的引用,而是将s1的数据复制到s2。
Mayur Ra​​iyani 2012年


4
Java中的每个变量都存储一个。并且由于类是引用类型,所以类类型变量中的值是reference。当您按照此答案或问题的第一条陈述进行分配时,您正在按复制参考。您没有通过引用复制任何内容。
亚当·罗宾逊

40

@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-创建一个新的SomeObjectclass实例,并将其分配给reference s1

在此处输入图片说明

SomeObject s2 = s1;

2-这将使参考s2s1指向所指向的对象。

在此处输入图片说明

s2.setText("second");

3-当在参考上使用setter时,它将修改参考所指向的对象。

在此处输入图片说明

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

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


1
很酷,+ 1为您的答案。尽管我想提出一些补充意见:1)图像可能会小一些。2)您应该为他的其他问题(如何复制SomeObject)添加答案,也许还有图片。
brimborium 2012年

16

当你这样做

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

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

可以这样想:您的房间里有一台电视机,但是有两个遥控器:使用哪个遥控器都没关系,您仍将对同一底层对象(电视机)进行更改。


5
+1,我喜欢电视遥控器的比喻。我猜原语是没有遥控器的电视,您必须在其中实际转到实际电视机并更改其值……还是其他?:)
Alex

10

当你写:

SomeObject s1 = new SomeObject("first");

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

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

因此,当您分配s1 = s2,然后调用s2更改对象的方法时,基础对象也会更改,并且在通过引用对象时可见s1

这是对象不变性的一种说法。通过使对象不可变,它们将不会在您之下改变,从而以更加可预测的方式运行。如果要复制对象,最简单/最实用的方法是编写一个copy()仅创建新版本并在字段上进行复制的方法。您可以使用序列化/反射等方法进行巧妙的复制,但这显然更为复杂。


+1提到不变性的概念。这与引用的概念一起,是此讨论中的关键。
Marvo 2012年

4

十大错误的Java程序员制作

6-混淆按值传递和按引用传递

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

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

因为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);

您从“ Java程序员犯的十大错误”中给出的引用不适合这种情况,因为它是在谈论基元而不是对象引用。
德罗纳

2

在代码中,s1s2相同的对象(您仅使用创建了一个对象new),然后s2在下一行中指向该对象。因此,在更改时text,如果通过s1和引用值,则两者都会更改s2

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


2

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

=操作者受让人指针值。它不会复制对象。

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


2

让我们从第二个示例开始:

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

SomeObject s1 = new SomeObject("first");

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

请注意,您尚未复制SomeObject,而是两个变量仅指向同一对象。因此,如果您正在修改s1s2,则实际上是在修改同一对象(请注意,如果您执行的操作类似s2 = new SomeObject("second")它们现在指向不同的对象)。

在第一个示例中,ab是原始值,因此修改一个不会影响另一个。

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


2

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

回答“此外,如果简单分配不能完成任务,如何复制SomeObject?” -尝试搜索cloneable(这是一个提供复制对象的方法的Java接口)和' copy constructors'(一种替代方法,并且可以说是更好的方法)


2

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


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

2
int a = new Integer(5) 

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

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

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

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

SomeObject s2 = s1;

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

如果您是Java用户,则可以与copy构造函数一起使用clone()方法复制对象。克隆可以按以下方式使用-

SomeObject s3 = s1.clone(); 

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


1

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

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


1

当给变量赋值和对象时,实际上是在给该对象分配引用。因此,两个变量s1s2都引用相同的对象。


1

由于对象是通过引用来引用的。因此,如果您编写s2 = s1,则仅复制引用。就像在C中,您仅使用内存地址进行处理。而且,如果您复制内存的地址并更改该地址后面的值,则两个指针(引用)都将在内存中的这一点更改一个值。


1

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.