加载中

In this article from my free Java 8 Course, I will be discussing the difference between a Deep and a Shallow CopyYou can download the slides and the article as PDF here.

What Is a Copy?

To begin, I’d like to highlight what a copy in Java is. First, let’s differentiate between a reference copy and an object copy. A reference copy, as the name implies, creates a copy of a reference variable pointing to an object. If we have a Car object, with a myCar variable pointing to it and we make a reference copy, we will now have two myCar variables, but still one object.

Example 1

An object copy creates a copy of the object itself. So if we again copied our car object, we would create a copy of the object itself, as well as a second reference variable referencing that copied object.

Example 2

本文来自于我提供的免费的 Java 8 课程,里面会针对深拷贝浅拷贝的不同之处进行讨论。你可以在这儿下载到PPT以及PDF文档

什么是拷贝?

开始之前,我要先强调一下 Java 中的拷贝是什么。首先,让我们对引用拷贝和对象拷贝进行一下区分。 引用拷贝, 正如它的名称所表述的意思, 就是创建一个指向对象的引用变量的拷贝。如果我们有一个 Car 对象,而且让 myCar 变量指向这个变量,这时候当我们做引用拷贝,那么现在就会有两个 myCar 变量,但是对象仍然只存在一个。

示例 1

对象拷贝会创建对象本身的一个副本。因此如果我们再一次服务我们 car 对象,就会创建这个对象本身的一个副本, 同时还会有第二个引用变量指向这个被复制出来的对象。

示例 2

What Is an Object?

Both a Deep Copy and a Shallow Copy are types of object copies, but what really is an object? Often, when we talk about an object, we speak of it as a single unit that can’t be broken down further, like a humble coffee bean. However, that’s oversimplified.

Example 3

Say we have a Person object. Our Person object is in fact composed of other objects, as you can see in Example 4. Our Person contains a Name object and an Address object. The Name in turn, contains a FirstName and a LastName object; the Address object is composed of a Street object and a City object. So when I talk about Person in this article, I’m actually talking about this entire network of objects.

Example 4

So why would we want to copy this Person object? An object copy, usually called a clone, is created if we want to modify or move an object, while still preserving the original object. There are many different ways to copy an object that you can learn about in another article. In this article we’ll specifically be using a copy constructor to create our copies.

什么是对象?

深拷贝和浅拷贝都是对象拷贝, 但一个对象实际是什么呢? 当我们谈论到对象时,我们经常会说它就像一粒浑圆的咖啡豆,已经是一个不能够被进一步分解的单位了,但这种说法太过于简化了。

示例 3

比方说我们有一个 Person 对象。这个 Person 对象实际上是由其它的对象组合而成的。如示例 4 所示, Person 对象包含了一个 Name 对象和一个 Address 对象。Name 对象又包含了一个 FirstName 对象和一个 LastName 对象;Address 对象又是由一个 Street 对象以及一个 City 对象组合而成的。那么当我们讨论本文中的这个 Person 时,实际上我是在讨论这些个对象所组成的整个的对象联系网络。

示例 4

那么为什么我会要对这个 Person 对象进行拷贝呢? 对象复制,经常也会被称作克隆,它是在我们想要修改或者移除某个对象,但仍然想要保留原来的那个对象时所要进行的操作。在另外一篇文章中你可以了解到许多拷贝一个对象的不同方法。在本文中我们将特别讲到如何利用拷贝构造器来创建拷贝。

Shallow Copy

First let’s talk about the shallow copy. A shallow copy of an object copies the ‘main’ object, but doesn’t copy the inner objects. The ‘inner objects’ are shared between the original object and its copy. For example, in our Person object, we would create a second Person, but both objects would share the same Name and Address objects.

Let’s look at a coding example. In Example 5, we have our class Person, which contains a Name and Address object. The copy constructor takes the originalPerson object and copies its reference variables.

public class Person {
    private Name name;
    private Address address;

    public Person(Person originalPerson) {
         this.name = originalPerson.name;
         this.address = originalPerson.address;
    }
[…]
}

Example 5

The problem with the shallow copy is that the two objects are not independent. If you modify the Name object of one Person, the change will be reflected in the other Person object.

Let’s apply this to an example. Say we have a Person object with a reference variable mother; then, we make a copy of mother, creating a second Person object, son. If later on in the code, the son tries to moveOut() by modifying his Address object, the mother moves with him!

Person mother = new Person(new Name(…), new Address(…));
[…]
Person son  = new Person(mother);
[…]
son.moveOut(new Street(…), new City(…));

Example 6

This occurs because our mother and son objects share the same Address object, as you can see illustrated in Example 7. When we change the Address in one object, it changes in both!

Example 7

浅拷贝

首先让我们来说说浅拷贝。对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。"里面的对象“会在原来的对象和它的副本之间共享。例如,我们会为一个 Person对象创建第二个 Person 对象, 而两个 Person 会共享相同的 Name 和 Address 对象。

让我们来看看代码示例。在示例 5 中,我们有一个类 Person,类里面包含了一个 Name 和 Address 对象。拷贝构造器会拿到 originalPerson 对象,然后对其应用变量进行复制。

public class Person {
    private Name name;
    private Address address;

    public Person(Person originalPerson) {
         this.name = originalPerson.name;
         this.address = originalPerson.address;
    }
[…]
}

示例 5

浅拷贝的问题就是两个对象并非独立的。如果你修改了其中一个 Person 对象的 Name 对象,那么这次修改也会影响奥另外一个 Person 对象。

让我们在示例中看看这个问题。假如说我们有一个 Person 对象,然后也会有一个引用变量 monther 来指向它;然后当我们对 mother 进行拷贝时,创建第二个 Person 对象 son。如果在此后的代码中, son 尝试用 moveOut() 来修改他的 Address 对象, 那么 mother 也会跟着他一起搬走!

Person mother = new Person(new Name(…), new Address(…));
[…]
Person son  = new Person(mother);
[…]
son.moveOut(new Street(…), new City(…));

示例 6

这种现象之所以会发生,是因为 mother 和son 对象共享了相同的 Address 对象,如你在示例 7 中所看到的描述。当我们在一个对象中修改了 Address 对象,那么也就表示两个对象总的 Address 都被修改了。

示例 7

Deep Copy

Unlike the shallow copy, a deep copy is a fully independent copy of an object. If we copied our Person object, we would copy the entire object structure.

Example 8

A change in the Address object of one Person wouldn’t be reflected in the other object as you can see by the diagram in Example 8. If we take a look at the code in example 9, you can see that we’re not only using a copy constructor on our Person object, but we are also utilizing copy constructors on the inner objects as well.

public class Person {
    private Name name;
    private Address address;

    public Person(Person otherPerson) {
         this.name    =  new Name(otherPerson.name);
         this.address =  new Address(otherPerson.address);
    }
[…]
}

Example 9

Using this deep copy, we can retry the mother-son example from Example 6. Now the son is able to successfully move out!

However, that’s not the end of the story. To create a true deep copy, we need to keep copying all of the Person object’s nested elements, until there are only primitive types and “Immutables” left. Let’s look at the Street class to better illustrate this:

public class Street {
    private String name;
    private int number;

    public Street(Street otherStreet){
         this.name = otherStreet.name;
         this.number = otherStreet.number;
    }
[…]
}

Example 10

The Street object is composed of two instance variables – String name and int number. int number is a primitive value and not an object. It’s just a simple value that can’t be shared, so by creating a second instance variable, we are automatically creating an independent copy. String is an Immutable. In short, an Immutable is an Object, that, once created, can never be changed again. Therefore, you can share it without having to create a deep copy of it.

深拷贝

不同于浅拷贝,深拷贝是一个整个独立的对象拷贝。如果我们对整个 Person对象进行深拷贝,我们会对整个对象的结构都进行拷贝。

示例 8

如你在示例 8 中所见,对一个 Person 的Address对象进行了修改并不会对另外一个对象造成影响。当我们观察示例 9 中的代码,会发现我们不单单对 Person 对象使用了拷贝构造器,同时也会对里面的对象使用拷贝构造器。

public class Person {
    private Name name;
    private Address address;

    public Person(Person otherPerson) {
         this.name    =  new Name(otherPerson.name);
         this.address =  new Address(otherPerson.address);
    }
[…]
}

示例 9

使用这种深拷贝,我们可以重新尝试示例 6 中的 mother-son 这个用例。现在 son 可以成功的搬走了!

不过,故事到这儿并没有结束。要创建一个真正的深拷贝,就需要我们一直这样拷贝下去,一直覆盖到 Person 对象所有的内部元素, 最后只剩下原始的类型以及“不可变对象(Immutables”。让我们观察下如下这个 Street 类以获得更好的理解:

public class Street {
    private String name;
    private int number;

    public Street(Street otherStreet){
         this.name = otherStreet.name;
         this.number = otherStreet.number;
    }
[…]
}

示例 10

Street 对象有两个实体变量组成 – String 类型的 name 以及 int 类型的 numberint  类型的 number 是一个原始类型,并非对象。它只是一个简单的值,不能共享, 因此在创建第二个实体变量时,我们可以自动创建一个独立的拷贝。String 是一个不可变对象(Immutable)。简言之,不可变对象也是对象,可一旦创建好了以后就再也不能被修改了。因此,你可以不用为其创建深拷贝就能对其进行共享。

Conclusion

To conclude, I’d like to talk about some coding techniques we used in our mother-son example. Just because a deep copy will let you change the internal details of an object, such as the Address object, it doesn’t mean that you should. Doing so would decrease code quality, as it would make the Person class more fragile to changes – whenever the Address class is changed, you will have to (potentially) apply changes to the Person class also. For example, if the Address class no longer contains a Street object, we’d have to change the moveOut() method in the Person class on top of the changes we already made to the Address class.

In Example 6 of this article I only chose to use a new Street and City object to better illustrate the difference between a shallow and a deep copy. Instead, I would recommend that you assign a new Address object instead, effectively converting to a hybrid of a shallow and a deep copy, as you can see in Example 10:

Person mother = new Person(new Name(…), new Address(…));
[…]
Person son  = new Person(mother);
[…]
son.moveOut(new Address(...));

Example 11

In object-oriented terms, this violates encapsulation, and therefore should be avoided. Encapsulation is one of the most important aspects of Object Oriented programming. In this case, I had violated encapsulation by accessing the internal details of the Address object in our Person class. This harms our code because we have now entangled the Person class in the Address class and if we make changes to the Address class down the line, it could harm the Person class as I explained above. While you obviously need to interconnect your various classes to have a coding project, whenever you connect two classes, you need to analyze the costs and benefits.

总结

作为总结,我要说说上面在 mother-son 示例中所用到的一些编码技术。只是因为深拷贝可以让你修改一个对象里面的详细信息,比如 Address 对象,这并不意味着你就该这样做。这样做会提高代码的质量, 因为它可以使得 Person 更容易修改 – 不管 Address 类什么时候被修改了,你也都会要修改应用到 Person 类。例如,如果 Address 类型不再包含 Street 对象了,我们就得根据已经对 Address 类做出的修改来对Person 类中的 moveOut()  方法进行修改。

在本文的示例 6 中,我只选择使用了一个新的 Street 和 City 对象,这样可以更好的对浅拷贝和深拷贝的不同之处进行描述。不过,我会建议你给方法分配一个新的 Address 对象,这样能有效的将其转换成一个浅拷贝和深拷贝的混合体, 见示例 10:

Person mother = new Person(new Name(…), new Address(…));
[…]
Person son  = new Person(mother);
[…]
son.moveOut(new Address(...));

示例 11

在面向对象领域,这样做违背了封装的原则,因此应该被避免。封装是面向对象编程中一个最重要的方面。在这里,我已经违背封装的原则,对 Person 类中 Address 对象的内部细节进行了访问。这样做对我们的代码造成了伤害,因为我们现在跟 Person 类中的 Address 类纠缠在一起,如果对 Address 类进行了修改,就会如我上面所解释的对代码造成伤害。不过是你显然是会需要将你定义的各种类互相联系在一起以构成代码工程的,但在你要将两个类联系在一起时,需要好好分析一下成本和收益。

返回顶部
顶部