Java笔记 ·

Java漫谈-深拷贝与浅拷贝

创建对象的方式

1、调用new语句创建对象,最常见的一种

2、运用反射手段创建对象,调用java.lang.Class 或者 java.lang.reflect.Constructor 类的newInstance()实例方

3、调用对象的clone()方法

4、使用反序列化,调用java.io.ObjectInputStream 对象的 readObject()方法.

将一个对象的引用复制给另外一个对象的方法

1、直接赋值 2、浅拷贝 3、深拷贝

直接赋值

实体类Person.java

public class Person {
    //姓名
    private String name;
    // 年龄
    private int age;
    // 编号
    private Integer code;

    public Person(){

    }
	
    public Person(String name, int age, Integer code) {
        this.name = name;
        this.age = age;
        this.code = code;
    }

    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 Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

}

测试类cloneDemo.java

本篇所有测试均会在该类下完成

public class cloneDemo {
    public static void main(String[] args) {
        Person p = new Person("小明",11,123);
        Person p1 = p;
        System.out.println(p);
        System.out.println(p1);
    }
}

打印结果:

Others.base.cloneDemo.Person@4554617c
Others.base.cloneDemo.Person@4554617c

两者打印出来的地址相同,可见二者的引用是同一个对象,并没有创建出一个新的对象。可以把这种现象叫做引用的复制(或引用拷贝)。

对Person.java追加toString方法,

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

cloneDemo.java中追加

        p1.setCode(1234);
        System.out.println("--------windcoder.com----------");
        System.out.println(p);
        System.out.println(p1);

从打印结果中更能体现出这一点,因为是同一个对象的引用,所以两者改一个,另一个对象的值也随之改变:

Person{name='小明', age=11, code=123}
Person{name='小明', age=11, code=123}
--------windcoder.com----------
Person{name='小明', age=11, code=1234}
Person{name='小明', age=11, code=1234}

浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

如果属性是基本类型,拷贝的就是基本类型的值;

如果属性是内存地址(引用类型),拷贝的就是内存地址(即复制引用但不复制引用的对象) ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

实现对象拷贝的类,必须实现Cloneable接口,并覆写clone()方法

下面改造Person.java

/**
 * 实现对象拷贝的类,必须实现Cloneable接口,并覆写clone()方法
 */
public class Person implements Cloneable{
    //姓名
    private String name;
    // 年龄
    private int age;
    // 编号
    private Integer code;

    public Person(){

    }

    public Person(String name, int age, Integer code) {
        this.name = name;
        this.age = age;
        this.code = code;
    }

    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 Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

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

    /**
     * 覆写clone()方法
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

cloneDemo.java中追加

        Person p2 = p;
        p2.setName("小明克隆人");
        p2.setAge(12);
        p2.setCode(12345);
        System.out.println(p);
        System.out.println(p2);
        System.out.println("--------windcoder.com----------");

得到结果:

Person{name='小明', age=11, code=1234}
Person{name='小明克隆人', age=12, code=12345}
--------windcoder.com----------

此时p2完成了一次拷贝。现在继续改造Person,添加一个Address对象。 Address.java

public class Address {
    private String name;

    public Address( ) {

    }

    public Address(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}

Person.java中添加对应属性

private Address address;
public Address getAddress() {
	return address;
}

public void setAddress(Address address) {
	this.address = address;
}
// 修改toString,追加address
public String toString() {
	return "Person{" +
			"name='" + name + '\'' +
			", age=" + age +
			", code=" + code +
			", address=" + address +
			'}';
}

cloneDemo.java追加如下:

p.setAddress(new Address("我是一个小学生"));
Person p2 = (Person)p.clone();
System.out.println(p);
System.out.println(p2);
System.out.println("--------windcoder.com----------");
p2.setName("小明克隆人");
p2.setAge(12);
p2.setCode(12345);
p2.getAddress().setName("我是一个小学生克隆人");
System.out.println(p);
System.out.println(p2);
System.out.println("--------windcoder.com----------");

我们预期是能够获得p还是原来的address,p2的address发生改变,但结果如何呢,打印发现两者都发生了改变:

--------windcoder.com----------
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
--------windcoder.com----------
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生克隆人'}}
Person{name='小明克隆人', age=12, code=12345, address=Student{name='我是一个小学生克隆人'}}
--------windcoder.com----------

这是由于,此时发生的仅是浅拷贝,即被拷贝对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。针对基本类型及其封装类都是将对应的基本类型值拷贝,对于其余对象,则仅是拷贝其引用,这导致最终里面的对象是同一个,更改一个,另一个的拷贝/原对象的对应值也随之更改。

深拷贝

如果想将上述的address也单独复制一份,这就用到了深拷贝。

被拷贝对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍

通俗的说,如果说浅拷贝,开始的时候是两条线,如果在最后有一个其他类的变量,那么这两条线最后会合二为一,共同指向这变量,都能对他进行操作。深拷贝则是完完全全的两条线,互不干涉,因为他已经把所有的内部中的变量的对象全都复制一遍了。

深拷贝在代码中,需要在clone方法中多书写调用这个类中其他类的变量的clone函数。

改造方案

Address.java 实现Cloneable接口并重写clone

public class Address implements Cloneable{
    private String name;

    public Address( ) {

    }

    public Address(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }

    /**
     * 覆写clone()方法
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
        protected Object clone()   {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Person.java修改clone方法:

    protected Object clone()   {
        try {
            // super.clone();
            // return super.clone();

            // 改为深复制:
            Person newPerson = (Person)super.clone();
            // 拷贝一份新的address
            newPerson.address = (Address) address.clone();
            return newPerson;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }

此时再运行之前的cloneDemo.java,发现结果已经是预期的效果:

--------windcoder.com----------
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
--------windcoder.com----------
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
Person{name='小明克隆人', age=12, code=12345, address=Student{name='我是一个小学生克隆人'}}
--------windcoder.com----------

拓展:利用串行化来做深复制

把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做 “解冻”或者“回鲜(depicking)”过程。

public Object deepClone() { 
 //写入对象 
 ByteArrayOutoutStream bo=new ByteArrayOutputStream(); 
 ObjectOutputStream oo=new ObjectOutputStream(bo); 
 oo.writeObject(this); 
 //读取对象
 ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray()); 
 ObjectInputStream oi=new ObjectInputStream(bi); 
 return(oi.readObject()); 
}

改造示例

Address追加Serializable接口实现

public class Address implements Serializable, Cloneable{
	.....
}

Person追加Serializable接口实现

public class Person implements Serializable, Cloneable{
	......
	/**
     * 利用串行化来做深复制
     * by: windCoder.com
     * @return
     */
    public Object deepClone()  {

        try {
            //写入对象
            ByteArrayOutputStream bo= new ByteArrayOutputStream();
            ObjectOutputStream os = new ObjectOutputStream(bo);
            os.writeObject(this);
            //读取对象
            ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
            ObjectInputStream oi=new ObjectInputStream(bi);
            return(oi.readObject());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

cloneDemo.java追加:

Person p3 = (Person)p.deepClone();
System.out.println(p);
System.out.println(p3);
System.out.println("--------windcoder.com----------");
p3.setName("小明克隆人");
p3.setAge(12);
p3.setCode(12345);
p3.getAddress().setName("我是一个小学生克隆人233333");
System.out.println(p);
System.out.println(p3);
System.out.println("--------windcoder.com----------");

运行结果:

--------windcoder.com----------
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
--------windcoder.com----------
Person{name='小明', age=11, code=1234, address=Student{name='我是一个小学生'}}
Person{name='小明克隆人', age=12, code=12345, address=Student{name='我是一个小学生克隆人233333'}}
--------windcoder.com----------

注:可以使用transient关键字,修饰不需要序列化的属性。当序列化对象的时候,这个属性就不会序列化到指定的目的地中。

参考资料

java创建对象的四种方式

java 深拷贝与浅拷贝机制详解

参与评论