深拷贝(deep clone)和浅拷贝(shallow copy)

这个又叫浅复制和深复制。
其相同点和区别:
相同点:
浅复制和深复制都是通过复制创建了一个新的对象,也就是 x.clone() != x
浅复制和深复制的对象所属的类是一致的,也就是 x.clone().getClass() == x.getClass()
浅复制和深复制的对象在某些情况下, 也就是从内容角度来说,它所包含的内容是一致的 x.clone().equals(x)

不同点:
我们知道 一个对象里面的成员变量这么几种: String, 8种基本数据类型,对其他对象的引用
对于 String, 8种基本数据类型,浅复制和深复制都是直接复制过来的,但是对于对象中所包含的对其他对象的引用的复制却有着巨大的差别
浅复制只是复制了对象中所包含的对其他对象的引用,而没有再复制引用所指向的实际的对象,也就是说浅复制复制出来的对象中的 对其他对象的引用还是指向的是被克隆对象中原来的对象,而深复制则是将对象中所包含的引用的对象也进行了复制

看下面的例子: Student 里面包含了一个对 Student2 对象的引用,如果进行浅复制,那么复制出来的对象 s2 是一个新的对象(与s1 内容相同,但是地址不同),并且 s2 中的对 Student2 对象的那个引用指向的还是 s1 对象的那个 Student2 对象(意思是只复制了对Student2的引用,并没有复制实际的对象,浅复制中s2的Student2对象和s1 中的Student2对象是相同的对象), 而深复制就是会将Student2对象实际复制一次,放在s2中(深复制中s2的Student2对象和s1 中的Student2对象是不同的对象

实现浅拷贝的方法:利用Object中的clone方法复制出来的对象都是浅拷贝。
首选 一个对象要实现浅拷贝,必须实现Cloneable接口(和Seriable接口一样是一个标记接口,看到这个接口就意味这是可以复制的)
其次: 重写clone方法, 在clone方法中的第一句调用 super.clone(), 并且可以声明为public的,扩大它的访问权限(在Object中的clone的访问权限是protect的)

其实在这里我是有疑问的,既然我们知道Object中的clone的访问权限是protect的,那么我们写的子类都是继承自Object的,那么 子类为什么还要实现Cloneable接口和重写 clone方法? 子类应该也是可以直接调用clone方法的呀?

这是因为对于Object类,它的clone方法实现如下:
protected native Object clone() throws CloneNotSupportedException;
如果用protected修饰,它的子类是可以访问的,所以子类调用的其实是Object的clone方法,而这个方法如果你在子类中不重写的话,永远抛出一个CloneNotSupportedException,所以 Object类的这个clone方法完全是为了子类覆盖它服务的,并不是让子类直接调用的,这也就是为什么在覆盖clone方法时一般到调用super.clone()的原因。 clone方法是可以被覆盖的,但是必须实现Cloneable接口,所以你覆盖后,它就不会调用父类的方法,而调用你覆盖后的方法。

    public Student clone() throws CloneNotSupportedException{
        Student s = (Student)super.clone();
        return s;
    }

疑问:为什么一定要调用super.clone方法?
在运行的时刻,Object中的clone( )会识别出你要复制的是哪一个对象,并且为此分配空间进行对象的复制。

//

public class ShallowCloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {

        Student2 student2 = new Student2(2, "lisi"); // 一个student2 类型的对象
        Student s1 = new Student(1, "zhangsan", student2);
        Student s2 = s1.clone();
        s2.getS2().setId(22);
        System.out.println(s2.getId()); // 1
        System.out.println(s2.getName()); // zhangsan
        System.out.println(s2.getS2().getId()); // 22
        System.out.println(student2.getId()); // 22
        Student s3 = s1.deepClone();
        s3.getS2().setId(33);
        System.out.println(s3.getS2().getId()); // 33
        System.out.println(student2.getId()); // 22

    }
}
class  Student implements Cloneable {
    private int id;
    private String name;
    private Student2 s2;
    public Student(int id, String name, Student2 s2) {
        this.id = id;
        this.name = name;
        this.s2 = s2;
    }
    public Student2 getS2() {
        return s2;
    }
    public void setS2(Student2 s2) {
        this.s2 = s2;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    // 浅克隆 ,
    public Student clone() throws CloneNotSupportedException{
        Student s = (Student)super.clone();
        return s;
    }
    // 只是实现了深拷贝的效果,并不是真正常规的深拷贝方法(如果Student2对象里面还有对象的引用,就无法实现深拷贝的效果了). 深拷贝通过序列化写入流再从流中读取出来
    public Student deepClone() throws Exception {
        Student s = (Student) super.clone();
        s.setS2(s.getS2().clone());
        return s;
    }

}
class  Student2 implements Serializable, Cloneable {
    private int id;
    private String name;
    public Student2(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

实现深克隆的方法: (通过序列化实现)

再回顾一下序列化的一些知识:(Serializable)
序列化:就是将对象转化到流的过程
反序列化:就是从流转化成对象中的过程
(注意的是不能序列化static 变量和 transient修饰的变量),但是如果你重写了writeObject() 和readObject()方法 ,序列化的时候就不再使用系统默认的序列方法,而是你自己在这两个方法里面实现序列化,那么transient修饰的变量也是可以被序列化的。

其实序列化的时候将对象放入到流中的时候就是放入的对象的一个副本,这个副本包含了里面所有的成员变量的值,即使是其他对象的引用,也会将所引用的实际对象复制出来放入到流中(因此可以知道,一个对象如果可以被序列化,意味着它里面的所引用的其他对象也是可以被序列化的)。

因此实现深复制的大致流程如下:(所以实现深复制有一个很重要的前提是 要复制的对象是可以被序列化的
第一步: 将要复制的对象写入到流中,
第二步: 从流中读取复制的对象

因此Teacher, Student ,Student2 必须都要实现序列化接口

public class DeepCloneTest {
    public static void main(String[] args) {
        Student2 student2 = new Student2(2, "lisi");  // 一个student2 类型的对象
        Student s = new Student(1,"zhangsan",student2);
        Teacher t = new Teacher("Teacher", s);
        Teacher tClone = t.clone(); 
    }
}
class Teacher implements Serializable {
    private static final long serialVersionUID = 7254176665661808754L;
    private String name;
    private Student student;
    public Teacher(String name, Student student) {
        this.name = name;
        this.student = student;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Student getStudent() {
        return student;
    }
    public void setStudent(Student student) {
        this.student = student;
    }
    public Teacher clone(){
        ByteArrayOutputStream out = new ByteArrayOutputStream();  // 节点流
        ObjectOutputStream oOut = null;
        try {
            oOut = new ObjectOutputStream(out);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } // 过滤流包含一个节点流
        try {
            oOut.writeObject(this);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
        ObjectInputStream iIn = null;
        try {
            iIn = new ObjectInputStream(in);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        Teacher t = null;
        try {
            t = (Teacher)iIn.readObject();
        } catch (ClassNotFoundException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        oOut.close();
        iIn.close();
        return t;
    }   
}

最后补充一点: 一般如果我们在 一个实现了序列化的接口中不定义serialVersionUID ,一般就会给我们一个警告:那么这个序列化中serialVersionUID 到底是干嘛用的呢

serialVersionUID 其实你可以看成一个识别码,主要是在你的类的信息被修改之后进行反序列化的过程中判断是不是同一个类型的对象,是否可以反序列化。
有的时候,可能出现一种情况,我们对类的成员变量做了修改,添加了某个变量,或者减少了某个变量,但是在接收到的流中的数据仍旧是未修改之前的数据,通过这个值,反序列的时候,可以判断是相同类型对象,如果添加了字段,那么反序列化的时候就给这个添加的字段赋予一个默认值,如果删除了某个字段,那么自动忽略掉这个字段。

serialVersionUID 一般有两种添加方式,
一种是随机设置一个值,比如1L,2L ;
另外一种就是系统根据你的可序列化的类的成员变量,成员方法,类信息等生成一个hash码。

    private static final long serialVersionUID = 1L;
    private static final long serialVersionUID = 7254176665661808754L;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值