Loading

刨析Object中的clone方法,涉及浅拷贝,深拷贝,原型模式

刨析Object中的clone方法,涉及浅拷贝,深拷贝,原型模式

说到java中的克隆,必要的一个方法就是Object类中native clone方法。

protected native Object clone() throws CloneNotSupportedException;

它是Object中的方法,这意味所有的类都可以实现这一方法。因为所有的类都隐式继承了Object。

注意:对象想要使用clone()必须要继承Cloneable接口.否则将抛出CloneNotSupportedException异常.

它一般在什么场景下使用呢?

比如说我们需要许多的奖状,同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,

难道我们要一个一个new 奖状("XX",msg); 这样一系列操作吗?

在参数较少的情况下,麻烦程度可能不太显现,那如果一个对象的参数设置就有许多,那就太麻烦了.

这时我们就可以使用clone方法快捷的创建对象。

就像这样:

Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);
//复制奖状
Citation c2 = c1.clone();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");

我们只需要复制多个“三好学生”奖 状出来,然后在修改奖状上的名字即可。

所以对于对象的创建非常复杂,我们可以使用clone()快捷的创建对象。

这也就是设计模式中原型模式的思想.

使用clone 和 浅克隆

举个例子:

我们有一个Man对象,继承Cloneable接口,重写clone方法,并使用clone方法复制.

class Man implements Cloneable {
    String name;
    int age;
	Son son;  //一个对象 其类拥有name和age属性
    
    public Man(String name, int age, Son son) {
        this.age = age;
        this.name = name;
        this.son = son;
    }

...省略name和age,son的get set方法 和重写的toString方法
    
//重写clone方法
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

Main方法:

public static void main(String[] args) throws CloneNotSupportedException {
	Son son = new son("小明",12);
    Man man1 = new Man("明他爸",35,son);//new 一个Man对象
    
    Man man2 = (Man) man1.clone(); //使用clone
    Man man3 = (Man) man1.clone();  

    System.out.println(man1);  //打印对象
    System.out.println(man2);
    System.out.println(man3);
    
    System.out.println(man1==man2);//判断是否是同一个对象
    System.out.println(man2==man3);
       
    System.out.println(man1.son==man2.son);//判断son属性对象地址是否相同
    System.out.println(man2.son==man3.son);
}

结果:

Man{name='明他爸', age=35, son=Son{name='小明', age=12}}  //属性值相同
Man{name='明他爸', age=35, son=Son{name='小明', age=12}}
Man{name='明他爸', age=35, son=Son{name='小明', age=12}}
false          //Main对象地址不相同
false
true           //son属性对象地址相同
true

可以看到使用clone()复制的对象,它们的属性值相等,但是对象地址不相同,意味这他们拥有不同的堆空间.不是同一个对象.

但是Main类中属性Son对象却是同一个对象.他们有相同的地址.

这也就是浅克隆.

结论:单纯使用clone即super.clone();创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有 属性所指向的对象的内存地址.

向上面使用浅克隆,复制出许多不同Man对象,而son对象却只有一个. 这么多爸爸只有一个儿子,那可就太惨了.🤣🤣🤣

当我修改对象中的非基本类型属性时,修改一个将会影响所有的克隆对象,因为对象中的非基本类型属性都是同一个.这将是一个非常严重的安全问题.

怎么解决呢?这就需要深克隆了.

深克隆是完完全全的克隆.复制整个对象信息,包含值类型和引用类型

两种深克隆的实现

深克隆的实现通常有两种方法:

  • 序列化实现深克隆:先将原对象序列化到内存的字节流中,再从字节流中反序列化出刚刚存储的对象,这个新对象和原对象就不存在任何地址上的共享,这样就实现了深克隆。
  • 所有引用类型都实现克隆:要复制对象的所有引用类型都要实现克隆,所有对象都是复制的新对象,从而实现了深克隆。

深克隆实现方式一:序列化(推荐)

实现思路:先将要拷贝对象写入到内存中的字节流中,然后再从这个字节流中读出刚刚存储的信息,作为一个新对象返回,那么这个新对象和原对象就不存在任何地址上的共享,自然实现了深拷贝。

注意类要实现 Serializable 接口

请参考以下代码:

class CloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        BirdChild birdChild = new BirdChild();
        birdChild.name = "小小鸟";
        Bird bird = new Bird();
        bird.name = "小鸟";
        bird.birdChild = birdChild;
        // 使用序列化克隆对象
        Bird bird2 = CloneUtils.clone(bird);
        bird2.name = "黄雀";
        bird2.birdChild.name = "小黄雀";
        System.out.println("bird name:" + bird.name);
        System.out.println("bird child name:" + bird.birdChild.name);
        System.out.println("bird name 2:" + bird2.name);
        System.out.println("bird child name 2:" + bird2.birdChild.name);
    }
}
class CloneUtils {
    public static <T extends Serializable> T clone(T obj) {
        T cloneObj = null;
        try {
            //写入字节流
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bo);
            oos.writeObject(obj);
            oos.close();
            //分配内存,写入原始对象,生成新对象
            ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流
            ObjectInputStream oi = new ObjectInputStream(bi);
            //返回生成的新对象
            cloneObj = (T) oi.readObject();
            oi.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

程序执行结果:

bird name:小鸟
bird child name:小小鸟
bird name 2:黄雀
bird child name 2:小黄雀

深克隆实现方式二:所有引用类型都实现克隆

学生类:

public class Student implements Cloneable {
    private String name;
    private int age;
    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    protected Object clone()  {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
    ...省略Student 的set  get方法

}

老师类:

public class Teacher implements Cloneable{
    private String name;
    private int age;
    private Student student;

    public Teacher(String name, int age, Student student){
        this.name = name;
        this.age = age;
        this.student = student;
    }
    ...省略Teacher类get set方法
    // 覆盖
    @Override
    public Object clone() {
        Teacher t = null;
        try {
            t = (Teacher) super.clone();
            t.student = (Student)student.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return t;
    }
 	
}

测试:

public class test {
    public static void main(String[] args) {
        Student s = new Student("学生1", 11);
        Teacher origin = new Teacher("老师原对象", 23, s);
        
        System.out.println("克隆前的学生姓名:" + origin.getStudent().getName());
        
        Teacher clone = (Teacher) origin.clone();//调用克隆方法
        
        // 更改克隆后的学生信息 更改了姓名
        clone.getStudent().setName("我是克隆对象更改后的学生2");
        System.out.println("克隆后的学生姓名:" + clone.getStudent().getName());
    }
}

运行结果:

克隆前的学生姓名:学生1
克隆后的学生姓名:我是克隆对象更改后的学生2

总结

  • 浅克隆:只会复制对象的值类型,而不会复制对象的引用类型;
  • 深克隆:复制整个对象,包含值类型和引用类型。
  1. 克隆可分为浅克隆和深克隆,实际应用中一般使用深克隆
  2. 深克隆有两种实现方法:
    • 使用序列化(推荐)
    • 所有引用类型都实现克隆
  3. 使用clone()方法需要实现Cloneable接口,否则将抛出CloneNotSupportedException异常.
  4. 原型模式就是使用克隆,快捷创建对象.
  5. Object中clone()方法是一个native 方法,也就是原生函数,本地方法,使用操作系统底层的语言实现的,因此执行效率更高

扩展

Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

还有什么问题小伙伴们可以留言讨论

posted @ 2022-03-22 16:29  程序员小小宇  阅读(234)  评论(0编辑  收藏  举报