深入理解jvm对象深浅拷贝

/ Java / 0 条评论 / 212浏览

深入理解jvm对象深浅拷贝

深浅拷贝图解

1. 引入

可以这样简单理解,将对象类型和普通数据类型都看成变量数据,浅拷贝就是数据引用传递,拷贝出来的数据在堆内存空间的地址没有发生变化,新旧对象使用的都是同一片地址,也就是同一个对象的引用,深拷贝就是在堆内存中重新开辟一块地址,新旧对象使用的是不同的引用,属性的修改互不影响

2. java中深浅拷贝的方法

(1) 构造方法
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class School{

    String schoolName;

    Teacher teacher;
    
    int rank;

}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Teacher {

    String teacherName;

}
public static void main(String[] args) {
        Teacher teacher = new Teacher("慢羊羊");
        School origin = new School("大肥羊学校", teacher,1);
        System.out.println("新建一个对象:"+origin);

        School clone = new School("大肥羊学校",new Teacher("慢羊羊"),1);
        System.out.println("clone:"+clone);
        System.out.println("origin:"+origin);

        System.out.println("修改原对象的数据");
        origin.setSchoolName("小肥羊学校");
        origin.getTeacher().setTeacherName("快羊羊");
        origin.setRank(2);
        System.out.println("clone:"+clone);
        System.out.println("origin:"+origin);
    }
新建一个对象:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
clone:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
origin:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
修改原对象的数据
clone:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
origin:School(schoolName=小肥羊学校, teacher=Teacher(teacherName=快羊羊), rank=2)

这里使用了构造方法重新新建了一个School对象,并且为其设置的Teacher属性也是new出来的新的对象,new也就是在jvm的堆内存空间开辟了一个存储地址存放新的对象数据,这里需要注意的是,在School中有下面三个属性:

    String schoolName;

    Teacher teacher;
    
    int rank;

其中包含普通数据类型和引用数据类型,普通数据类型不存在深拷贝和签拷贝,属于值传递,String类型比较特殊,String属于引用数据类型,但是由于String是final修饰的字符数组,所以是无法改变的,当进行复制的时候就不存在修改的问题,需要改变String变量的值时只能重新new一个字符串。最后就是其他引用对象了,比如这里的Teacher。

上面的程序运行可以看出,我们成功的拷贝了一个对象,但其实等于我们重新new了一个新的School对象,然后为这个新的对象赋值了旧对象的所有相同的属性,如果属性是引用数据类型,我们也是同样new了一个新的作为其属性。这样的方法肯定不是我们想要的,因为这就全部都是程序原自己在重新构造对象了。

(2) clone方法——深拷贝

在Object类中有一个native方法:

protected native Object clone() throws CloneNotSupportedException;

所以java中所有的类都可以调用这个方法,但是有一个要求,调用的类本身需要实现Cloneable接口

public interface Cloneable {
}

可以看到该接口没有任何代码,根据经验,这就是一个典型的标记接口,可以解释为什么Object的clone方法会throws CloneNotSupportedException,只有当调用clone方法的类实现了Cloneable接口才支持,否则就会抛出异常

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class School implements Cloneable{

    String schoolName;

    Teacher teacher;

    int rank;

    public School doClone() throws CloneNotSupportedException {
        Teacher teacherClone = this.teacher.doClone();
        School schoolClone = (School) super.clone();
        schoolClone.setTeacher(teacherClone);
        return schoolClone;
    }

    @Override
    protected School clone() throws CloneNotSupportedException {
        Teacher teacherClone = this.teacher.clone();
        School schoolClone = (School) super.clone();
        schoolClone.setTeacher(teacherClone);
        return schoolClone;
    }
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Teacher implements Cloneable{

    String teacherName;

    public Teacher doClone() throws CloneNotSupportedException {
        return (Teacher) super.clone();
    }

    @Override
    protected Teacher clone() throws CloneNotSupportedException {
        return (Teacher) super.clone();
    }
}
public static void main(String[] args) throws CloneNotSupportedException {
        Teacher teacher = new Teacher("慢羊羊");
        School origin = new School("大肥羊学校", teacher,1);
        System.out.println("新建一个对象:"+origin);

        School clone = origin.clone();
        System.out.println("clone:"+clone);
        System.out.println("origin:"+origin);

        System.out.println("修改原对象的数据");
        origin.setSchoolName("小肥羊学校");
        origin.getTeacher().setTeacherName("快羊羊");
        origin.setRank(2);
        System.out.println("clone:"+clone);
        System.out.println("origin:"+origin);
    }
新建一个对象:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
clone:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
origin:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
修改原对象的数据
clone:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
origin:School(schoolName=小肥羊学校, teacher=Teacher(teacherName=快羊羊), rank=2)

可以看到,也可以进行深复制,上面我实现了一个doClone方法并且重写了clone方法,其实两种只需要使用一种就可以了,很多博客文章都是说重写Object类的clone方法,但事实上,只需要在克隆的时候调用Object的clone()即可,方法可以自己定义也可以重写父类Object的clone()。父类Object中的clone方法使用的是protect修饰的,所以只有相同包下(我们可以不考虑)和子类才可以调用,这样设计我认为是Object中的clone需要克隆一个对象,但是不是Object对象自己,需要克隆的都是子类,所以干脆定义为protect(public会导致任何类都可以调用,会容易包异常,private就更不行了,子类也无法调用)

上面写的clone是进行对象的深拷贝,可以发现对象的引用数据类型Teacher这个属性也是进行了深拷贝,这是因为我们也重写了Teacher的clone方法,然后再School的clone方法中调用Teacher的clone方法,就等于重新new了一个Teacher对象赋值给了School,从某种角度来说,和上面的使用构造方法,程序员自己构造一个完全的新对象方法是基本一致的。

直接调用简单重写的clone()方法(直接调用父类Object的clone)的子类A只能克隆自己的非引用数据类型,如果需要为引用数据类型的属性也进行拷贝,那么该引用数据类型的属性的类B也需要重写clone方法,并且需要在A的clone方法中调用B的clone方法获取拷贝的新属性并进行赋值,否则就是浅拷贝(即默认的clone拷贝都是浅拷贝,因为默认的只是直接调用父类的clone,没有进行其他的属性赋值),如下。

(3) clone方法——浅拷贝

比如我们直接将School的clone方法设置如下


@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class School implements Cloneable{

    String schoolName;

    Teacher teacher;

    int rank;

    //在这里,我直接调用的只是School的clone,没有管引用数据类型Teacher(所以Teacher是否实现Colneable和是否重写了clone都无关紧要)
    @Override
    protected School clone() throws CloneNotSupportedException {
        return (School) super.clone();
    }
}

执行后

新建一个对象:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
clone:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
origin:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=慢羊羊), rank=1)
修改原对象的数据
clone:School(schoolName=大肥羊学校, teacher=Teacher(teacherName=快羊羊), rank=1)
origin:School(schoolName=小肥羊学校, teacher=Teacher(teacherName=快羊羊), rank=2)

可以看到,修改原始对象的数据后,新克隆的对象的引用数据类型属性teacher同步发生了变化,说明进行的是浅拷贝

(4) 序列化和发序列化进行深拷贝

上面说的几种方法,都需要使用到原对象,在java中还可以通过对象的序列化和反序列化来进行对象的深拷贝,关于序列化的只是可以参考我的另一篇博客


public static void main(String[] args) throws IOException, ClassNotFoundException {
        Teacher teacher = new Teacher("慢羊羊");
        School origin = new School("大肥羊学校", teacher,1);
        System.out.println("新建一个对象:"+origin);

        //创建一个字节输入流,用于存储后面的序列化对象(不清楚的可以了解下ByteOutputStream源码,其中包含一个字节数组缓冲区存储数据(也有扩机制),作为输出流的目的地)
        ByteOutputStream outputStream = new ByteOutputStream();
        //创建一个对象输出流,进行对象的序列化(不了解的可以参考我的另一篇博客)
        ObjectOutputStream objOut = new ObjectOutputStream(outputStream);
        //将序列化对象写入字节数组缓冲区
        objOut.writeObject(origin);
 
        //从字节数组缓冲区获取对象的序列化数据
        byte[] bytes = outputStream.getBytes();
        //将对象的序列化数据存入字节输入流的字节数组缓冲区  
        ByteInputStream inputStream = new ByteInputStream(bytes,bytes.length);
        //创建一个对象输入流,对输入流的源头的数据进行读取并反序列化为对象  
        ObjectInputStream objIn = new ObjectInputStream(inputStream);
        School clone = (School) objIn.readObject();

        System.out.println("clone:"+clone);
        System.out.println("origin:"+origin);

        System.out.println("修改原对象的数据");
        origin.setSchoolName("小肥羊学校");
        origin.getTeacher().setTeacherName("快羊羊");
        origin.setRank(2);
        System.out.println("clone:"+clone);
        System.out.println("origin:"+origin);

    }

同样的,是否能够序列化,类需要实现Serializable接口,所以上面的School和Teacher都实现了序列化接口.

3. 总结

综合上面所说的,可以看出来,其实java中对象的深拷贝就是new出来一个新的对象,保证新的对象和原对象的所有属性都是一样的即可,前几种使用构造方法或者使用clone方法就是通过java中的已有的api来完成这个环节,后面的序列化和反序列化的方式其实也是new一个新的对象(开辟一个新的内存空间),只是属性的数据都是读取的保存的原对象的序列化的数据,所以说,其实不仅是使用java自带的序列化方式,使用json进行对象的转换(或者xml,protobuf,或者直接存取数据库),传输也从某种意义上来看也是属于对象的拷贝,只是从一个jvm进程拷贝到另一个jvm进程.

以上就是我对java中深拷贝的理解,有不同观点的童鞋欢迎下方留言交流哦