Java深(Deep)拷贝与浅(Shadow)拷贝
基本代码
//代码清单1 Address.java
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
//浅拷贝,则Adress不需要实现Cloneable接口
public class Address implements Cloneable {
private String country;/**国家*/
private String province;/**省*/
private String city;/**城市*/
public Address(String country,String province,String city){
this.country = country;
this.province = province;
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//代码清单2 Person.java
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Person implements Cloneable {
private String name;
private String gender;
private Address address;
public Person(String name, String gender, Address address) {
this.name = name;
this.gender = gender;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
/**
* 浅拷贝
*/
//return super.clone();
/**
* 深拷贝
*/
Person p = (Person)super.clone();
//把所有是引用类型的字段再拷贝一次
address = (Address) p.getAddress().clone();
return p;
}
}
浅拷贝
浅拷贝(shadow copy)即按位拷贝对象。新对象通过精确拷贝源对象值的方式被创建出来。假如对象具有引用类型的字段,那么只会拷贝引用地址(内存地址;reference addresses or memory address)。
如上图,结合代码清单1和2。在上图中person拥有字段name、gender和address,其中address是引用类型。对person进行浅拷贝后产生person2,person2仍然指向address。
经过观察发现,原始数据类型经过浅拷贝后,会生成新的一份数据,上图中是name->name1;gender->gender2。而address是一个Address对象,是引用类型,所以经过浅拷贝后仍旧会指向原来的address。
因此,person内的address经过任何改变,也会在person2中反映出来。
代码实现:
- 需要实现Cloneable接口。
- 重写clone()方法,返回super.clone();
//代码清单3
public class Main {
public static void main(String[] args) throws CloneNotSupportedException{
/**待拷贝的源对象*/
Address address = new Address("中国","广西省","崇左市");
Person person = new Person("张三","男",address);
print("person => %s",person);
/**拷贝*/
Person person2 = (Person)person.clone();//拷贝效果看Person的clone()方法的内容。
print("person2 => %s",person2);
/**
* 测试浅/深拷贝特性:
* 1. 如果源拷贝对象有引用类型的字段,则改变引用类型的字段值,会既影响到源拷贝对象,又影响到clone后的对象
* 2. 改变原始类型字段值,则不会影响到双方
*/
address.setProvince("湖南省");
address.setCity("长沙市");
person.setName("李四");
print("\n改变引用类型字段值后:");
print("person => %s",person);
print("person2 => %s",person2);
}
public static void print(String format,Object... args){
System.out.println(String.format(format,args));
}
}
output
person => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
改变引用类型字段值后:
person => Person(name=李四, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))
person2 => Person(name=张三, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))
观察输出,浅拷贝结论是:
- clone重写只需要返回super.clone();
- 源对象和拷贝对象内的引用数据类型字段指向的都是同一个地方。即该字段在任意一个地方改变,都会如实反映在各个拷贝或源对象中。
深拷贝
深拷贝拷贝所有的字段(Fields),并且拷贝字段指向的动态分类的内存。当一个对象连同它指向的对象被拷贝时,深拷贝就发生了。
在上图中,person拥有字段name、gender、address。当对person进行深拷贝时,name1包含复制name的值,同理gender1和address1都包含复制的gender和address值。address发生任何改变,都不会在address1内发生改变。
代码实现:
- 需要实现Cloneable接口(源对象内的引用类型也许要实现Cloneable接口,并重写clone()方法)。
- 重写clone()方法。先调用要拷贝对象的clone()方法,然后在调用源对象内引用对象的clone()方法。
1. 如代码清单2,下面是一些片段
/**
* 深拷贝
*/
Person p = (Person)super.clone();
//把所有是引用类型的字段再拷贝一次
address = (Address) p.getAddress().clone();
return p;
2. 测试代码和代码清单3一模一样
output
person => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
改变引用类型字段值后:
person => Person(name=李四, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
person2 => Person(name=张三, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))
深拷贝结论:
- 源对象和拷贝对象之间互不影响。无论字段是否是应用类型还是原始数据类型。
序列化实现深拷贝
注意,对象内的所有类都必须实现序列化接口。
1. 代码清单1、2中
Address.java 、Person.java需要实现Serializable接口
//测试代码如下
public class Main {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
try {
// create original serializable object
Address address = new Address("中国", "广西省", "崇左市");
Person person = new Person("张三", "男", address);
// print it
print("person => %s", person);
// deep copy
ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
// serialize and pass the object
oos.writeObject(person);
oos.flush();
ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bin);
// return the new object
Person person2 = (Person) ois.readObject();
// verify it is the same
print("person2 => %s", person2);
// change the original object's contents
address.setProvince("湖南省");
address.setCity("长沙市");
person.setName("李四");
// see what is in each one now
print("\n改变引用类型字段值后:");
print("person => %s", person);
print("person2 => %s", person2);
} catch (Exception e) {
System.out.println("Exception in main = " + e);
} finally {
oos.close();
ois.close();
}
}
public static void print(String format,Object... args){
System.out.println(String.format(format,args));
}
}
output
person => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
改变引用类型字段值后:
person => Person(name=李四, gender=男, address=Address(country=中国, province=湖南省, city=长沙市))
person2 => Person(name=张三, gender=男, address=Address(country=中国, province=广西省, city=崇左市))
不足之处(缺点、局限性)
- 不能序列化一个transient变量。
- 如果是单例模式,你以这种方式进行深拷贝,则它就不再是单例模式了。
- 性能问题。创建一个socket,序列化一个对象,然后通过socket传递它,最后反序列化。不建议这样用,因为它比实现Cloneable慢100倍左右。
懒拷贝(lazy copy)
懒拷贝实际上是深拷贝和浅拷贝的结合体。当初始化的时候,用浅拷贝。一个计数器需要用来追踪有多少个对象共享数据,当应用更改源对象时,它知道数据是否共享,并决定是否要做深拷贝。
懒拷贝从结果看,像极了深拷贝,只是它充分利用了浅拷贝的优势——速度快。当源对象内的引用数据类型字段不经常被更改时,用浅拷贝。
缺点是追踪counter的花销,此外,在特殊情况下,循环引用也会导致问题。
总结
使用场景有:
使用浅拷贝:对象只有原始数据类型字段;有引用类型的字段,但从不更改它。
使用深拷贝:有引用类型的字段,且经常被修改。
简言之,使用哪种拷贝方式看需求如何。
其实也可以手动进行深拷贝,或者浅拷贝,即
import org.springframework.beans.BeanUtils
//Copy the property values of the given source bean into the target bean.
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, null, (String[]) null);
}
参考来源