数组的浅复制,为什么不能简单地使用newArr = oldArr?

6

假设我有一个整数数组"orig"

我想进行浅拷贝,那么我可以这样做:

int[] shallow = orig;

我的教授说对于原始类型,浅复制和深复制本质上是相同的,因为我们必须复制数组的每个索引。 但将整个数组设置为另一个数组会做同样的事情,对吗?

我对对象数组也有类似的问题

这是我的思想

Book[] objArr2 = objArr1;

但是我被告知需要将每个数组索引复制过去,像这样

//for loop
objArr2[i] = objArr1[i];

对于浅复制来说,将数组等于另一个数组和逐个复制每个数组索引之间是否真的有什么区别呢?(我知道深复制意味着您必须创建全新的对象)

在浅复制中,这两种方法没有本质区别。两种方法都只是复制了数组本身以及其中存储的引用。如果数组包含对象,则这些对象的引用也将被复制。因此,原始数组和副本数组中的对象引用将指向相同的内存地址。


6
这并不是复制,而是赋值,两者之间有很大的区别。对一个数组所做的所有更改都将完全反映在另一个数组中,因为当你这样做时,实际上只有一个数组对象。 - Hovercraft Full Of Eels
你可能没有意识到你的复制是多么浅。你只是复制了一个指向唯一数组的引用 - Marko Topolnik
2
int[] 不是原始类型,而是一个数组。 - Greg Hewgill
引用和浅拷贝有什么区别? - onepiece
数组(或任何对象)的赋值通常不被称为数组/对象的“复制”,实际上它只是复制了对该数组/对象的引用。这个链接可能会有所帮助:https://dev59.com/O2025IYBdhLWcg3wKCf- - leonbloy
显示剩余2条评论
4个回答

18

我想要浅拷贝它,那么我能不能只做这个操作:

int[] shallow = orig;

这并不是一个真正的浅拷贝。一个拷贝应该是一个与原始对象相似但不是原始对象本身的离散实体。在你的例子中,实际上你有两个指向同一对象的引用。当你创建一个拷贝时,你应该得到两个结果对象:原始对象和拷贝对象。

在这里,任何你对shallow所做的修改也会发生在orig上,因为它们都指向同一个对象。

“浅度”是当你比较的对象在内部有引用其他对象时发挥作用。例如,如果你有一个整数数组并创建一个副本,你现在有两个包含相同整数值的数组:

Original Array

[0]
[1]
[2]
[3]

After copying:

[0] <--- Original  [0]
[1]                [1]
[3]                [2]
[4]      Copy ---> [3]

然而,如果你有一个由对象组成的数组(比如说objArr1objArr2),那么当你进行浅拷贝时,你现在有了两个新的数组对象,但是两个数组之间相应位置的条目指向同一个对象(因为对象本身没有被复制,只有引用被复制)。

Original Array:

[0:]----> [object 0]
[1:]----> [object 1]
[2:]----> [object 2]
[3:]----> [object 3]

复制后(注意相应位置指向相同实例):

Original -> [0:]----> [object 0] <----[:0] <- Copy
            [1:]----> [object 1] <----[:1]
            [2:]----> [object 2] <----[:2]
            [3:]----> [object 3] <----[:3]

现在,如果您通过替换条目或删除条目来修改objArr1,那么同样的事情不会发生在objArr2上。然而,如果您修改了objArr1 [0]处的对象,则 objArr2 [0]中也会反映出来,因为这些位置指向相同的对象。因此,在这种情况下,即使容器对象本身是不同的,它们包含的是对同一对象的引用。

当您进行深层复制时,您将获得两个新数组,其中每个对应位置指向不同的实例。因此,基本上您要一路复制对象。

我的教授说,对于基元类型(shallow和deep copy)本质上是相同的,我们必须复制数组的每个索引。

需要强调的区别是,当您复制基元类型的数组时,您只需完全复制值即可。每次都会得到一个新的基元类型。但是,当您有一个对象数组时,您所拥有的是一个对象引用数组。因此,当您创建副本时,您所做的就是创建一个具有原始数组中引用的副本。然而,这些引用的新副本仍然指向相应的相同对象。这就是所谓的浅复制。如果您深度复制了数组,那么每个单独位置引用的对象也将被复制。因此,您会看到以下内容:

Original -> [0:]----> [object 0] Copy -> [0:]----> [copy of object 0]
            [1:]----> [object 1]         [1:]----> [copy of object 1]
            [2:]----> [object 2]         [2:]----> [copy of object 2]
            [3:]----> [object 3]         [3:]----> [copy of object 3]

但是将整个数组等于另一个数组并不会产生相同的效果,对吧?

不,不会。你所做的只是创建了一个对现有数组的新引用:

arr1 -> [0, 1, 2, 3, 4]

现在假设你执行了arr2 = arr1。你所拥有的是:

arr1 -> [0, 1, 2, 3, 4] <- arr2

所以这里arr1arr2都指向同一个数组。因此,使用arr1进行的任何修改都将在使用arr2访问数组时反映出来,因为您正在查看相同的数组。当你复制时这种情况不会发生。


1
问题在于你需要将数组视为一个对象。你在objArr1中存储的是一个内存地址,该地址引用了数组的开头。例如,如果objArr1数组存储在地址0x1234578处,则objArr1的值实际上是0x1234578,当你说
objArr2 = objArr1;

你的意思是objArr2的值应该等于0x1234578。现在,如果你改变存储在0x1234578处的数组的第一个元素,那么无论你使用objArr1[1]还是objArr2[1]引用它,值都将相同。如果你尝试改变它们中的任何一个,结果也是一样的:你对其中一个所做的任何事情都会反映在另一个上,因为它们指向同一个位置。例如,以下代码将是正确的:

objArr2 = objArr1;
objArr2[0] = 5;
objArr1[0] = 6;
System.out.println(objArr2[0]); //prints "6"

有时候这种行为是有用的,但并不会导致复制。

1

针对基本类型

考虑以下示例:

public class Example {

    //this class just uses the reference as you suggest
    public static class ArrayEater {

        private final int[] food;

        public ArrayEater(final int[] food) {
            this.food = food; // references same array, does not crate copy
        }

        public void eat() {
            for (int index = 0; index < food.length; index++) {
                final int bite = food[index];
                if (bite == 0) {
                    System.out.println("No food at position " + index);
                } else {
                    System.out.println("Eating " + bite + " from position " + index);
                }
                food[index] = 0;
            }
        }
    }

    //this class makes an actual copy
    public static class ArrayCopyThenEatEater {

        private final int[] food;

        public ArrayCopyThenEatEater(final int[] food) {
            this.food = new int[food.length]; // creates new array
            for (int index = 0; index < food.length; index++) { //copies over the values
                this.food[index] = food[index];
            }
        }

        public void eat() {
            for (int index = 0; index < food.length; index++) {
                final int bite = food[index];
                if (bite == 0) {
                    System.out.println("No food at position " + index);
                } else {
                    System.out.println("Eating " + bite + " from position " + index);
                }
                food[index] = 0;
            }
        }
    }

    public static void main(String[] args) {

        int[] originalArray = {1,3,6,9};
        ArrayEater eater = new ArrayEater(originalArray);
        eater.eat(); 
        eater.eat(); 
        for (int index = 0; index < originalArray.length; index++) {
            System.out.println("Original array has value of " + originalArray[index] + " at position " + index);
        }

        originalArray = new int[]{1,3,6,9};
        ArrayCopyThenEatEater copyEater = new ArrayCopyThenEatEater(originalArray);
        copyEater.eat(); 
        copyEater.eat(); 
        for (int index = 0; index < originalArray.length; index++) {
            System.out.println("Original array has value of " + originalArray[index] + " at position " + index);
        }
    }

}

如果您查看主方法,您会发现创建了一个originalArray并传递给两个“eaters”。其中一个“eater”只是像您建议的那样引用原始数组,另一个“eater”创建了一个实际的副本(就像您的教授试图说的那样,这个副本对于原始类型来说既是浅层次的又是深层次的)。

运行此代码将创建以下输出:

Eating 1 from position 0
Eating 3 from position 1
Eating 6 from position 2
Eating 9 from position 3
No food at position 0
No food at position 1
No food at position 2
No food at position 3
Original array has value of 0 at position 0  <-- here we see that the eater ate the original!!
Original array has value of 0 at position 1
Original array has value of 0 at position 2
Original array has value of 0 at position 3
Eating 1 from position 0
Eating 3 from position 1
Eating 6 from position 2
Eating 9 from position 3
No food at position 0
No food at position 1
No food at position 2
No food at position 3
Original array has value of 1 at position 0 <-- here we see that the eater did not eat the original!!
Original array has value of 3 at position 1
Original array has value of 6 at position 2
Original array has value of 9 at position 3

从上面可以看出,您的方法会导致原始数组的内容被覆盖,而您的教授建议的复制方法不会改变原始数组。

对于非基本类型

这里,我们有一个类Food,它要么被吃掉了,要么没有。我们有三个食客,分别用于引用复制、浅复制和深复制:

public class Example {

    public static class Food implements Cloneable {
        private boolean eaten = false;
        public void eat() {
            eaten = true;
        }
        public boolean isEaten() {
            return eaten;
        }
        public Food clone() {
            try {
                return (Food) super.clone();
            } catch (CloneNotSupportedException e) {
                return null; //we won't get here
            }
        }
    }

    public static class ReferenceEater {

        private final Food[] food;

        public ReferenceEater(final Food[] food) {
            this.food = food; // references same array, does not crate copy
        }

        public void eat() {
            for (int index = 0; index < food.length; index++) {
                final Food bite = food[index];
                if (bite.isEaten()) {
                    System.out.println("No food at position " + index);
                } else {
                    System.out.println("Eating from position " + index);
                    bite.eat();
                }
            }
        }
    }

    public static class ShallowEater {

        private final Food[] food;

        public ShallowEater(final Food[] food) {
            this.food = new Food[food.length]; // creates new array
            for (int index = 0; index < food.length; index++) {
                this.food[index] = food[index]; //shallow copy still references same elements!
            }
        }

        public void eat() {
            for (int index = 0; index < food.length; index++) {
                final Food bite = food[index];
                if (bite.isEaten()) {
                    System.out.println("No food at position " + index);
                } else {
                    System.out.println("Eating from position " + index);
                    bite.eat();
                }
            }
        }
    }

    public static class DeepEater {

        private final Food[] food;

        public DeepEater(final Food[] food) {
            this.food = new Food[food.length]; // creates new array
            for (int index = 0; index < food.length; index++) {
                this.food[index] = food[index].clone(); //deep copy also copies the elements!
            }
        }

        public void eat() {
            for (int index = 0; index < food.length; index++) {
                final Food bite = food[index];
                if (bite.isEaten()) {
                    System.out.println("No food at position " + index);
                } else {
                    System.out.println("Eating from position " + index);
                    bite.eat();
                }
            }
        }
    }

    public static void main(String[] args) {

        Food[] originalArray = {new Food(), new Food(), new Food()};
        ReferenceEater referenceEater = new ReferenceEater(originalArray);
        referenceEater.eat(); 
        referenceEater.eat(); 
        for (int index = 0; index < originalArray.length; index++) {
            System.out.println("Food at position " + index + " has been eaten?  " + originalArray[index].isEaten());
        }

        originalArray = new Food[]{new Food(), new Food(), new Food()};
        ShallowEater shallowEater = new ShallowEater(originalArray);
        shallowEater.eat(); 
        shallowEater.eat(); 
        for (int index = 0; index < originalArray.length; index++) {
            System.out.println("Food at position " + index + " has been eaten?  " + originalArray[index].isEaten());
        }

        originalArray = new Food[]{new Food(), new Food(), new Food()};
        DeepEater deepEater = new DeepEater(originalArray);
        deepEater.eat(); 
        deepEater.eat(); 
        for (int index = 0; index < originalArray.length; index++) {
            System.out.println("Food at position " + index + " has been eaten?  " + originalArray[index].isEaten());
        }
    }

}

请注意,深拷贝还会复制数组的各个元素。
现在看一下输出结果:
Eating from position 0
Eating from position 1
Eating from position 2
No food at position 0
No food at position 1
No food at position 2
Food at position 0 has been eaten?  true
Food at position 1 has been eaten?  true
Food at position 2 has been eaten?  true
Eating from position 0
Eating from position 1
Eating from position 2
No food at position 0
No food at position 1
No food at position 2
Food at position 0 has been eaten?  true
Food at position 1 has been eaten?  true
Food at position 2 has been eaten?  true
Eating from position 0
Eating from position 1
Eating from position 2
No food at position 0
No food at position 1
No food at position 2
Food at position 0 has been eaten?  false
Food at position 1 has been eaten?  false
Food at position 2 has been eaten?  false

在这里,我们可以看到,就像原始数据类型一样,引用复制又一次导致了原始数据的消失。但是,如果我们看浅层复制,我们会发现与上面原始数据类型的深/浅层复制相同,这次所有的食物都已经被吃掉了(不像原始数据类型!)。这是因为虽然我们创建了一个新的数组,但我们传递了对相同Food实例的引用。最后,通过深度复制,我们可以看到原始数组中的食物项没有被吃掉,因为吃者吃的是那些项的克隆。


0

你的例子使用了数组,所以我会继续使用它们作为示例。数组实际上只是基于类型的“分区”内存块。像 int[] a 这样的值,是该数组的起始内存地址。当你执行 int[] a = someOtherArray 时,你将为它分配另一个内存位置的地址,而不是它们存储的值。因此,你必须逐个元素地赋值每个位置存储的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接