数组复制的方式和深拷贝、浅拷贝的区别

引言:

java中对象的拷贝分两种:深拷贝和浅拷贝

深拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用。

  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
  • 深拷贝:在计算机中开辟了一块新的内存地址用于存放复制的对象。实现深拷贝的两种方式是重写对象的Object方法(继承自Object)或者使用对象的序列化

拷贝数组

下面看一段代码,假设要把数组a1的数组拷贝到a2

@Test
    public void test1(){
        int[] a1 = {1,2,3,4};
        int[] a2 = a1;
        a2[0] = 66;
        System.out.println("a1: " + Arrays.toString(a1));
        System.out.println("a2: " + Arrays.toString(a2));
    }
 
output:
a1: [66, 2, 3, 4]
a2: [66, 2, 3, 4]

假设a1指向的内存区域是A,仅仅将a1的引用赋值给a2,a1和a2都指向A,对a2的修改会直接导致内存区域A的数据发生改变

如果数组中都是基本数据类型

要完成数组的复制(拷贝数组的修改对原数组不造成影响),一般有三种方法可以选择:

  • for /while循环,一个一个复制,这个就不多说了;
  • 使用System类中的静态方法arraycopy()或者Arrays类中的copyOf();从源码中可以看出Arrays.copy()实际上也是调用了System.arraycopy(),相当于简化版的System方法
  • 数组对象的clone方法
    下面看代码实例:
@Test
    public void test2(){
        int[] a1 = {1,2,3,4};
        System.out.println("原始的a1: " + Arrays.toString(a1));
        int[] a2 = new int[a1.length];
        System.arraycopy(a1,0,a2,0,a1.length);
        int[] a3 = Arrays.copyOf(a1,a1.length);
        int[] a4 = a1.clone();
        a2[1] = 222;
        a3[2] = 333;
        a4[3] = 444;
        System.out.println("修改后的a1: " + Arrays.toString(a1));
        System.out.println("System a2: " + Arrays.toString(a2));
        System.out.println("Arrays.copyOf a3: " + Arrays.toString(a3));
        System.out.println("clone a4: " + Arrays.toString(a4));
    }
 
output:
原始的a1: [1, 2, 3, 4]
修改后的a1: [1, 2, 3, 4]
System a2: [1, 222, 3, 4]
Arrays.copyOf a3: [1, 2, 333, 4]
clone a4: [1, 2, 3, 444]

System.arrayCopy()的源代码说明:

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

参数说明:

Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度

如果数组中都是引用类型

上面的方法就失效了,看下面代码

//省略构造函数和toString()方法
private class Person{
        int age;
        String name;
}
@Test
    public void test3(){
        Person p1 = new Person(1,"1号");
        Person p2 = new Person(2,"2号");
        Person p3 = new Person(3,"3号");
        Person p4 = new Person(4,"4号");
        Person[] a1 = {p1,p2,p3,p4};
        System.out.println("原始的a1: " + Arrays.toString(a1));
        Person[] a2 = new Person[a1.length];
        System.arraycopy(a1,0,a2,0,a1.length);
        Person[] a3 = Arrays.copyOf(a1,a1.length);
        Person[] a4 = a1.clone();
        a2[1].age = 222;a2[1].name = "222号";
        a3[2].age = 333;a3[2].name = "333号";
        a3[3].age = 444;a3[3].name = "444号";
        System.out.println("修改后的a1: " + Arrays.toString(a1));
        System.out.println("System a2: " + Arrays.toString(a2));
        System.out.println("Arrays.copyOf a3: " + Arrays.toString(a3));
        System.out.println("clone a4: " + Arrays.toString(a4));
    }
 

output:

数组复制的方式和深拷贝、浅拷贝的区别

可以看到我们只修改了拷贝数组中对象,原数组的对象也发生了改变,这就是浅拷贝,拷贝的都是原对象的引用,对引用指向的内存区域进行修改当然会使原对象发生改变。

那么问题来了,如果数组中是引用对象,如何拷贝能使原对象不受拷贝对象的影响,下面我们看看如何实现

需要复制的类重写clone方法(继承自Object)

//省略构造函数和toString()
private class Person implements Cloneable{
        int age;
        String name;
        @Override
        protected Object clone() {
            Object o = null;
            try {
                o = super.clone();
            } catch (CloneNotSupportedException e) {
                e.printStackTrace();
            }
            return o;
        }
    }

测试代码如下:

@Test
    public void test4(){
        System.out.println("=======================浅拷贝===========================");
        Person p1 = new Person(1,"1号");
        Person p2 = new Person(2,"2号");
        Person[] a1 = {p1,p2};
        System.out.println("原始的a1: " + Arrays.toString(a1));
        Person[] a2 = Arrays.copyOf(a1,a1.length);
        a2[0].age = 111;
        a2[0].name = "111号";
        System.out.println("a2修改后的a1: " + Arrays.toString(a1));
        System.out.println("a2: " + Arrays.toString(a2));
        System.out.println("=======================深拷贝===========================");
        Person p3 = new Person(3,"3号");
        Person p4 = new Person(4,"4号");
        Person[] a3 = {p3,p4};
        System.out.println("原始的a3: " + Arrays.toString(a3));
        Person[] a4 = new Person[a3.length];
        for(int j = 0 ; j < a3.length ; j++){
            a4[j] = (Person) a3[j].clone();
        }
        a4[0].age = 333;
        a4[0].name = "333号";
        System.out.println("a4修改后的a3: " + Arrays.toString(a3));
        System.out.println("a4: " + Arrays.toString(a4));
    }
output:
=======================浅拷贝===========================
原始的a1: [Person{age=1, name='1号'}, Person{age=2, name='2号'}]
a2修改后的a1: [Person{age=111, name='111号'}, Person{age=2, name='2号'}]
a2: [Person{age=111, name='111号'}, Person{age=2, name='2号'}]
=======================深拷贝===========================
原始的a3: [Person{age=3, name='3号'}, Person{age=4, name='4号'}]
a4修改后的a3: [Person{age=3, name='3号'}, Person{age=4, name='4号'}]
a4: [Person{age=333, name='333号'}, Person{age=4, name='4号'}]

上述代码中,我们将拷贝后的Person对象放入拷贝数组,内存中开辟了一块新的区域存放新的Person对象,之后对拷贝对象所有的操作都与原对象无关,仅仅是对拷贝对象的修改

序列化方法

把对象写到流里的过程是序列化过程Serialization),而把对象从流中读出来的过程则叫做反序列化过程Deserialization)。

应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。

在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。

这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,就需要仔细考察那些不可串行化的对象可否设成transient,从而将其排除在复制过程之外。

注意Cloneable与Serializable接口都是marker Interface,也就是说它们只是标识接口,没有定义任何方法。

//省略了构造方法和toString()
private class Person implements Serializable {
        int age;
        String name;
        public Object deepClone() throws Exception{
            // 序列化
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            // 反序列化
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return ois.readObject();
        }
    }

测试方法:

//需要注意的是:这里Person作为一个内部类,当实现Serializable接口时,外部类也要实现该接口否则会报错NotSerializableException
@Test
public void test5() throws Exception {
        Person p1 = new Person(1,"1号");
        Person p2 = new Person(2,"2号");
        Person[] a1 = {p1,p2};
        System.out.println("原始的a1: " + Arrays.toString(a1));
        Person[] a2 = new Person[a1.length];
        for(int i = 0 ; i < a1.length ; i++){
            a2[i] = (Person) a1[i].deepClone();
        }
        a2[1].age = 333;
        a2[1].name = "333号";
        System.out.println("a1: " + Arrays.toString(a1));
        System.out.println("a2: " + Arrays.toString(a2));
 
    }
 
output:
原始的a1: [Person{age=1, name='1号'}, Person{age=2, name='2号'}]
a1: [Person{age=1, name='1号'}, Person{age=2, name='2号'}]
a2: [Person{age=1, name='1号'}, Person{age=333, name='333号'}]

补充

实现Serialable接口时的serialVersionUID的作用

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

serialVersionUID有两种显示的生成方式:

  • 默认的1L,比如:private static final long serialVersionUID = 1L
  • 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: private static final long serialVersionUID = xxxxL

如果你的对象序列化后存到硬盘上面后,你却更改了类的field(增加或减少或改名),当你反序列化时,就会出现异常,这样就会造成不兼容性的问题。但当serialVersionUID相同时,它就会将不一样的field以type的缺省值Deserialize,这个可以避开不兼容性的问题。

假设Person类序列化之后,从A端传输到B端,然后在B端进行反序列化。在序列化Person和反序列化Person的时候,不同的情况会出现怎样的结果?

数组复制的方式和深拷贝、浅拷贝的区别

关于serialVersionUID 更详细的内容可以参考这篇博客java类中serialversionuid 作用 是什么