Java中对象拷贝,对象的浅拷贝、对象的深拷贝

2年前 (2022) 程序员胖胖胖虎阿
305 0 0

文章目录

  • 什么是对象拷贝?对象的浅拷贝、对象的深拷贝?
    • 对象的拷贝的说明
    • 浅拷贝
      • 1.使用类的构造器实现浅拷贝
      • 2.重写Object.clone()实现浅拷贝
    • 深拷贝
      • 1.重写Object.clone实现深拷贝
      • 2.序列化实现深拷贝
  • 使用序列化进行拷贝对象的注意事项

什么是对象拷贝?对象的浅拷贝、对象的深拷贝?

对象的拷贝的说明

Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。

  • 例:比如,对象A和对象B都属于类C,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a; B.b=A.b;

拷贝对象,主要是为了在新的上下文环境中复用现有对象的部分或全部 数据。

Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)

铺垫知识:Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

浅拷贝

浅拷贝(Shallow Copy)

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

浅拷贝示例图:
Java中对象拷贝,对象的浅拷贝、对象的深拷贝

以下两种方法实现浅拷贝

1.使用类的构造器实现浅拷贝

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

public class ObjectShallowCopyByConstructor {


    public static void main(String[] args) {

        //**************通过类的构造器浅拷贝对象**************
        Person personZhang = new Person("张三", 22, new Occupation("程序员",8000));
        Person personLi = new Person(personZhang);
        System.out.println("=====改变值前=====");
        System.out.println(personZhang.getOccupation().hashCode());
        System.out.println(personZhang);
        System.out.println(personLi.getOccupation().hashCode());
        System.out.println(personLi);
        System.out.println("=====改变值后=====");
        personZhang.setAge(30);
        personZhang.setName("李四");
        personZhang.getOccupation().setOccName("厨师");   //改变personZhang的Occupation的occName
        System.out.println(personZhang.getOccupation().hashCode());
        System.out.println(personZhang);
        System.out.println(personLi.getOccupation().hashCode());
        System.out.println(personLi);
    }

}
class Person{
    private String name;
    private int age;
    private Occupation occupation;  //职业

    public Person(){}

    public Person(String name, int age, Occupation occupation) {
        this.name = name;
        this.age = age;
        this.occupation = occupation;
    }
    //拷贝构造器,用于拷贝对象
    public Person(Person person) {
        this.name = person.name;
        this.age = person.age;
        this.occupation = person.occupation;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Occupation getOccupation() {
        return occupation;
    }

    public void setOccupation(Occupation occupation) {
        this.occupation = occupation;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", occupation=" + occupation +
                '}';
    }
}
class Occupation{
    private String occName;
    private int salary;

    public Occupation() {
    }
    public Occupation(String occName, int salary) {
        this.occName = occName;
        this.salary = salary;
    }

    public String getOccName() {
        return occName;
    }

    public void setOccName(String occName) {
        this.occName = occName;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Occupation{" +
                "occName='" + occName + '\'' +
                ", salary=" + salary +
                '}';
    }
}

运行结果:
Java中对象拷贝,对象的浅拷贝、对象的深拷贝
简析:

  • 可以看到,在改变对象1的基本数据类型的成员变量的值时,对象2的对应的成员变量并没有随之而改变,因为是进行的值传递。
  • 但在改变对象1的引用数据类型的成员变量occupation时,对象2的occupation也随之改变了,因为对象1和对象2的occupation都指向了同一个实例(引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。)
  • String类型的name比较特殊,这是因为String类型虽然是引用数据类型,但是它是存放在JVM内存中的常量池中的,常量池中的数据是不可更改的,在改变对象1的name的时候,其实是在常量池里新创建了“李四”这个String,然后对象1的name指向常量池中的“李四”,对象2的还是指向原来那个“张三”。

使用类的构造器这种方式来浅拷贝,需要拷贝的成员变量多了后,代码会很臃肿。。

2.重写Object.clone()实现浅拷贝

Object是所有类的根父类,所有类都继承Object这个类,Object类中有一个protected Object clone() throws CloneNotSupportedException的方法,这个方法就是进行的浅拷贝,我们可以通过调用clone()方法来进行浅拷贝。
注意:

  • Object类虽然有这个方法,但是这个方法是受保护的(被protected修饰),所以我们无法直接使用。
  • 使用clone方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。

对于这两点,解决方法是在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。

代码:

public class ObjectShallowCopyByClone {


    public static void main(String[] args) {

        //**************通过重写Object.clone方法,来浅拷贝对象**************
        Person personZhang = new Person("张三", 22, new Occupation("程序员",8000));
        Person personLi = (Person) personZhang.clone();
        System.out.println("=====改变值前=====");
        System.out.println(personZhang.getOccupation().hashCode());
        System.out.println(personZhang);
        System.out.println(personLi.getOccupation().hashCode());
        System.out.println(personLi);
        System.out.println("=====改变值后=====");
        personZhang.setAge(30); //尝试改变personZhang的属性值,观察拷贝对象personLi中的值会不会有什么变化
        personZhang.setName("李四");
        personZhang.getOccupation().setOccName("厨师");   //改变personZhang的Occupation的occName
        System.out.println(personZhang.getOccupation().hashCode());
        System.out.println(personZhang);
        System.out.println(personLi.getOccupation().hashCode());
        System.out.println(personLi);
    }

}

class Person implements Cloneable{
    private String name;
    private int age;
    private Occupation occupation;  //职业

    public Person(){}

    public Person(String name, int age, Occupation occupation) {
        this.name = name;
        this.age = age;
        this.occupation = occupation;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Occupation getOccupation() {
        return occupation;
    }

    public void setOccupation(Occupation occupation) {
        this.occupation = occupation;
    }

    //重写Object的clone方法
    @Override
    public Object clone(){
        Object obj = null;
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", occupation=" + occupation +
                '}';
    }
}

class Occupation{
    private String occName;
    private int salary;

    public Occupation() {
    }
    public Occupation(String occName, int salary) {
        this.occName = occName;
        this.salary = salary;
    }

    public String getOccName() {
        return occName;
    }

    public void setOccName(String occName) {
        this.occName = occName;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Occupation{" +
                "occName='" + occName + '\'' +
                ", salary=" + salary +
                '}';
    }
}

运行结果:
Java中对象拷贝,对象的浅拷贝、对象的深拷贝


深拷贝

深拷贝涉及到对象图的概念

  • 对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
  • 简而言之,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建新的内存空间

因为创建内存空间和拷贝整个对象图,所以深拷贝相比于浅拷贝速度较慢并且花销较大。

深拷贝示例图:
Java中对象拷贝,对象的浅拷贝、对象的深拷贝

深拷贝的实现方法主要有以下两种:

1.重写Object.clone实现深拷贝

代码:

public class ObjectDeepCopyByClone {
    public static void main(String[] args) {

        //**************通过重写Object.clone方法,来深拷贝对象**************
        Person personZhang = new Person("张三", 22, new Occupation("程序员",8000));
        Person personLi = (Person) personZhang.clone();
        System.out.println("=====改变值前=====");
        System.out.println(personZhang.getOccupation().hashCode());
        System.out.println(personZhang);
        System.out.println(personLi.getOccupation().hashCode());
        System.out.println(personLi);
        System.out.println("=====改变值后=====");
        personZhang.setAge(30); //尝试改变personZhang的属性值,观察拷贝对象personLi中的值会不会有什么变化
        personZhang.setName("李四");
        personZhang.getOccupation().setOccName("厨师");   //改变personZhang的Occupation的occName
        System.out.println(personZhang.getOccupation().hashCode());
        System.out.println(personZhang);
        System.out.println(personLi.getOccupation().hashCode());
        System.out.println(personLi);
    }
}

class Person implements Cloneable{
    private String name;
    private int age;
    private Occupation occupation;  //职业

    public Person(){}

    public Person(String name, int age, Occupation occupation) {
        this.name = name;
        this.age = age;
        this.occupation = occupation;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Occupation getOccupation() {
        return occupation;
    }

    public void setOccupation(Occupation occupation) {
        this.occupation = occupation;
    }

    //重写Object的clone方法
    @Override
    public Object clone(){
        Object obj = null;
        try {
            obj = super.clone();
            //把当前对象中引用类型对象的属性值赋值给拷贝对象中的引用类型的对象
            ((Person)obj).setOccupation(new Occupation(this.occupation.getOccName(),this.occupation.getSalary()));
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", occupation=" + occupation +
                '}';
    }
}

class Occupation{
    private String occName;
    private int salary;

    public Occupation() {
    }
    public Occupation(String occName, int salary) {
        this.occName = occName;
        this.salary = salary;
    }

    public String getOccName() {
        return occName;
    }

    public void setOccName(String occName) {
        this.occName = occName;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Occupation{" +
                "occName='" + occName + '\'' +
                ", salary=" + salary +
                '}';
    }
}

运行结果:
Java中对象拷贝,对象的浅拷贝、对象的深拷贝
简析:

可以看到完成了对象的深拷贝,即使改变了对象1的引用类型的成员变量的值,对象2的也没有随之改变,因为两个对象都指向不同地址了嘛,且两个对象中的引用对象也是指向不同的地址。

2.序列化实现深拷贝

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

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

public class ObjectDeepCopyBySerializable {
    public static void main(String[] args){

        //**************通过让类实现Serializable接口,来深拷贝对象**************
        ByteOutputStream bos = null;
        ObjectOutputStream oos = null;
        ByteInputStream bis = null;
        ObjectInputStream ois = null;

        try {
            Person personZhang = new Person("张三", 22, new Occupation("程序员",8000));

            bos = new ByteOutputStream();
            oos = new ObjectOutputStream(bos);
            oos.writeObject(personZhang);
            oos.flush();
            ois = new ObjectInputStream(bos.newInputStream());
            Person personLi = (Person) ois.readObject();

            System.out.println("=====改变值前=====");
            System.out.println(personZhang.getOccupation().hashCode());
            System.out.println(personZhang);
            System.out.println(personLi.getOccupation().hashCode());
            System.out.println(personLi);
            System.out.println("=====改变值后=====");
            personZhang.setAge(30); //尝试改变personZhang的属性值,观察拷贝对象personLi中的值会不会有什么变化
            personZhang.setName("李四");
            personZhang.getOccupation().setOccName("厨师");   //改变personZhang的Occupation的occName
            personZhang.getOccupation().setSalary(5000);   //改变personZhang的Occupation的salary
            System.out.println(personZhang.getOccupation().hashCode());
            System.out.println(personZhang);
            System.out.println(personLi.getOccupation().hashCode());
            System.out.println(personLi);

        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (oos != null){
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }


    }
}

class Person implements Serializable {
    private String name;
    private int age;
    private Occupation occupation;  //职业

    public Person(){}

    public Person(String name, int age, Occupation occupation) {
        this.name = name;
        this.age = age;
        this.occupation = occupation;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Occupation getOccupation() {
        return occupation;
    }

    public void setOccupation(Occupation occupation) {
        this.occupation = occupation;
    }


    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", occupation=" + occupation +
                '}';
    }
}

class Occupation implements Serializable{
    private String occName;
    private int salary;

    public Occupation() {
    }
    public Occupation(String occName, int salary) {
        this.occName = occName;
        this.salary = salary;
    }

    public String getOccName() {
        return occName;
    }

    public void setOccName(String occName) {
        this.occName = occName;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Occupation{" +
                "occName='" + occName + '\'' +
                ", salary=" + salary +
                '}';
    }
}

运行结果:
Java中对象拷贝,对象的浅拷贝、对象的深拷贝

使用序列化进行拷贝对象的注意事项

静态成员变量、被transient修饰的成员变量 在序列化时是不会被序列化的。
详见下文。。。
Java中的transient关键字

相关文章

暂无评论

暂无评论...