JAVA - 浅拷贝与深拷贝

时间:2022-06-15 19:48:54

背景知识:拷贝

JAVA中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。举例说明:比如,对象A和对象B都属于类S,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a;  B.b=A.b;

引用拷贝:指创建一个指向对象的引用变量的拷贝

public static void copyReferenceObject(){
    	Car myCar1 = new Car("Benz", 2000);
    	Car myCar2 = myCar1;
        System.out.println(myCar1);
        System.out.println(myCar2);
}

如果我们有一个 Car 对象,而且让 myCar1变量指向这个变量,这时候当我们做引用拷贝,那么现在就会有两个变量myCar1和myCar2,但是对象仍然只存在一个

JAVA - 浅拷贝与深拷贝

对象拷贝:创建对象本身的一个副本。因此如果我们再一次服务我们 car 对象,就会创建这个对象本身的一个副本, 同时还会有第二个引用变量指向这个被复制出来的对象。

JAVA - 浅拷贝与深拷贝

JAVA的对象拷贝分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)

浅拷贝和深拷贝都是针对一个已有对象的操作

* 浅拷贝Shallow Copy)

对象的浅拷贝会对原始对象进行拷贝,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝,但不会复制原始对象里面的对象。“里面的对象”会在原来的对象和它的副本之间共享

①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据

②对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用变量拷贝,也就是只是将该成员变量的引用值(引用对应的对象内容的内存地址)复制一份给新的对象。因为实际上两个对象的该引用数据类型的成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量引用的对象会影响到另一个对象的该成员变量引用的对象

JAVA - 浅拷贝与深拷贝

浅拷贝的实现方式:

通过拷贝构造方法进行浅拷贝

拷贝构造方法指的是该类的构造方法参数为该类的对象。使用拷贝构造方法可以很好地完成浅拷贝,直接通过一个现有的对象创建出与该对象属性相同的新的对象

public class Test {
    public static void main(String[] args) {
        Person p1=new Person(20,"HAPPY");
        Person p2=new Person(p1);
        p1.setAddr("load");
        System.out.println(p1);
        System.out.println(p2);
    }
}

class Person{
    private int age;
    private String name;
    private Address addr;

	public Person(int age,String name) {
        this.age=age;
        this.name=name;
        this.addr=new Address("street");
    }
    
    //拷贝构造方法,方法体中将所有属性都进行赋值
    public Person(Person p) {
        this.name=p.name;
        this.age=p.age;
        this.addr=p.addr;
    }    
    
    public void setAddr(String addr) {
		this.addr.setAddress(addr);
	}

    public String toString() {
        return this.name+this.age+this.addr.toString();
    }
}   

class Address{
	private String address;
	
	public Address(String addr) {
		this.address=addr;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public String toString() {
		return address;
	}	
}

运行结果:

HAPPY20load

HAPPY20load

分析:

Person类里面包含了一个Address对象。拷贝构造方法会拿到p1对象,然后对其成员变量进行复制。浅拷贝的问题就是两个对象并非独立的。如果修改了其中一个Person对象的Address对象,也会影响到另外一个Person对象。

②重写clone()方法

Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,不能在类外进行访问,所以要想对一个对象进行复制,就需要对clone方法覆盖

重写clone()方法的步骤:

* 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常) 该接口为标记接口(不含任何方法)

* 重写clone()方法,访问修饰符设为public。方法中通过调用super.clone()方法得到Object类中的原clone方法来复制对象 

public class Test {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("HAPPY",a,175);
        
        //通过调用重写后的clone方法进行浅拷贝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        
        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大*");
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        
        //使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
        stu1.setaAge(new Age(66));           
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

class Age{
    //年龄类的成员变量(属性)
    private int age;
    //构造方法
    public Age(int age) {
        this.age=age;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public String toString() {
        return this.age+" ";
    }
}

class Student implements Cloneable{
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;
    //构造方法,其中一个参数为另一个类的对象
    public Student(String name,Age a,int length) {
        this.name=name;
        this.aage=a;
        this.length=length;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Age getaAge() {
        return this.aage;
    }
    
    public void setaAge(Age age) {
        this.aage=age;
    }
    
    public int getLength() {
        return this.length;
    }
    
    public void setLength(int length) {
        this.length=length;
    }

    public String toString() {
        return "姓名是: "+this.getName()+", 年龄为: "+this.getaAge().toString()+", 长度是: "+this.getLength();
    }
    
    //重写Object类的clone方法
    public Object clone() {
        Object obj=null;
        //调用Object类的clone方法,返回一个Object实例
        try {
            obj= super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

运行结果:

姓名是: HAPPY, 年龄为: 20 , 长度是: 175

姓名是: HAPPY, 年龄为: 20 , 长度是: 175

姓名是: 大*, 年龄为: 99 , 长度是: 216

姓名是: HAPPY, 年龄为: 99 , 长度是: 175

姓名是: 大*, 年龄为: 66 , 长度是: 216

姓名是: HAPPY, 年龄为: 99 , 长度是: 175

* 深拷贝(Deep Copy)

深拷贝不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷深拷贝相比于浅拷贝速度较慢并且花销较大

JAVA - 浅拷贝与深拷贝

拷贝的实现方式:

通过拷贝构造方法进行深拷贝

要创建一个真正的深拷贝,不单单对一个类使用了拷贝构造器,同时也会对里面的对象的类使用拷贝构造器一直这样拷贝下去,一直覆盖到一个类所有的内部元素, 最后只剩下原始的类型以及“不可变对象(Immutables)”

public class Test {
    public static void main(String[] args) {
        Person p1=new Person(20,"HAPPY");
        Person p2=new Person(p1);
        p1.setAddr("load");
        System.out.println(p1);
        System.out.println(p2);
    }
}

class Person{
    private int age;
    private String name;
    private Address addr;

	public Person(int age,String name) {
        this.age=age;
        this.name=name;
        this.addr=new Address("street");
    }
    
    //拷贝构造方法,方法体中将所有属性都进行赋值
    public Person(Person p) {
        this.name=p.name;
        this.age=p.age;
        this.addr=new Address(p.addr);
    }    
    
    public void setAddr(String addr) {
		this.addr.setAddress(addr);
	}

    public String toString() {
        return this.name+this.age+this.addr.toString();
    }
}   

class Address{
	private String address;
	
	public Address(String addr) {
		this.address=addr;
	}
	
	//拷贝构造方法
	public Address(Address addr) {
		this.address=addr.address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public String toString() {
		return address;
	}	
}

运行结果:

HAPPY20load

HAPPY20street

②重写clone方法

与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用引用变量clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝

public class Test {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("HAPPY",a,175);
        
        //通过调用重写后的clone方法进行深拷贝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        
        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("SAD");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

class Age implements Cloneable{
    //年龄类的成员变量(属性)
    private int age;
    //构造方法
    public Age(int age) {
        this.age=age;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public String toString() {
        return this.age+"";
    }
    
    //重写Object的clone方法
    public Object clone() {
        Object obj=null;
        try {
            obj=super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

class Student implements Cloneable{
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age age;
    private int length;
    //构造方法,其中一个参数为另一个类的对象
    public Student(String name,Age a,int length) {
        this.name=name;
        this.age=a;
        this.length=length;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Age getaAge() {
        return this.age;
    }
    
    public void setaAge(Age age) {
        this.age=age;
    }
    
    public int getLength() {
        return this.length;
    }
    
    public void setLength(int length) {
        this.length=length;
    }
    public String toString() {
        return "姓名是: "+this.getName()+", 年龄为: "+this.getaAge().toString()+", 长度是: "+this.getLength();
    }
    
    //重写Object类的clone方法
    public Object clone() {
        Object obj=null;
        //调用Object类的clone方法——浅拷贝
        try {
            obj= super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        //调用Age类的clone方法进行深拷贝
        //先将obj转化为学生类实例
        Student stu=(Student)obj;
        //学生类实例的Age对象属性,调用其clone方法进行拷贝
        stu.age=(Age)stu.getaAge().clone();
        return obj;
    }
}

运行结果:

姓名是: HAPPY, 年龄为: 20, 长度是: 175

姓名是: HAPPY, 年龄为: 20, 长度是: 175

姓名是: SAD, 年龄为: 99, 长度是: 216

姓名是: HAPPY, 年龄为: 20, 长度是: 175

3通过对象序列化实现深拷贝

虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。

将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException  {
        Age a=new Age(20);
        Student stu1=new Student("HAPPY",a,175);
        
        Student stu2=stu1.copy();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());

        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("SAD");
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}
/*
 * 创建年龄类
 */class Age implements Serializable{
    //年龄类的成员变量(属性)
    private int age;
    //构造方法
    public Age(int age) {
        this.age=age;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    public String toString() {
        return this.age+"";
    }
}/*
 * 创建学生类
 */class Student implements Serializable{
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;
    //构造方法,其中一个参数为另一个类的对象
    public Student(String name,Age a,int length) {
        this.name=name;
        this.aage=a;
        this.length=length;
    }
    //eclipe中alt+shift+s自动添加所有的set和get方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public Age getaAge() {
        return this.aage;
    }
    
    public void setaAge(Age age) {
        this.aage=age;
    }
    
    public int getLength() {
        return this.length;
    }
    
    public void setLength(int length) {
        this.length=length;
    }
    //设置输出的字符串形式
    public String toString() {
        return "姓名是: "+this.getName()+", 年龄为: "+this.getaAge().toString()+", 长度是: "+this.getLength();
    }
    
	//通过序列化方法实现深拷贝
    public Student copy() throws IOException, ClassNotFoundException {
    	ByteArrayOutputStream bos=new ByteArrayOutputStream();
        ObjectOutputStream oos=new ObjectOutputStream(bos);
        oos.writeObject(this);
        oos.flush();
        ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        return (Student)ois.readObject();
    }
 }

运行结果:

姓名是: HAPPY, 年龄为: 20, 长度是: 175

姓名是: HAPPY, 年龄为: 20, 长度是: 175

姓名是: SAD, 年龄为: 99, 长度是: 216

姓名是: HAPPY, 年龄为: 20, 长度是: 175

注:如果某个属性被transient修饰,那么该属性就无法通过序列化被拷贝