Java 深拷贝,浅拷贝

一直听说这两个词,确实不知道代表啥意思?也不知道究竟要用来做什么?什么时候用到他们。

下面是从一篇博文种得到的解释:

浅复制(浅克隆) :被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。

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

在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制。

有两种方式:(目前还不知道怎么做)
1). 实现Cloneable接口并重写Object类中的clone()方法;
2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆;

实现clone方法的步骤
(1)实现Cloneable接口
(2)重载Object类中的clone()方法,重载时需定义为public
(3)在重载方法中,调用super.clone()

 package lesson1211;

 public class Student implements Cloneable {   //不实现Cloneable接口,编译不会报错,但是运行时会报异常,所以必须实现Cloneable接口
private int number; //浅复制
/* (non-Javadoc)
* @see java.lang.Object#clone()
*/
@Override
protected Object clone(){ Student stu = null;
try {
stu = (Student)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return stu;
} /**
* @return the number
*/
public int getNumber() {
return number;
} /**
* @param number the number to set
*/
public void setNumber(int number) {
this.number = number;
}
} package lesson1211; public class TestClone { public static void main(String[] args) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("stu1: " + stu1.getNumber());
System.out.println("stu2: " + stu2.getNumber()); stu2.setNumber(45678); System.out.println("stu1: " + stu1.getNumber());
System.out.println("stu2: " + stu2.getNumber());
}
}

解释:
(1)clone()方法是定义在java.lang.Object类中,该方法是一个protected的方法,所以重载时要把clone()方法的属性设置为public,这样其它类才能调用这个clone类的clone()方法
(2)实现Cloneable接口:Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException异常。

浅克隆

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。
Java 深拷贝,浅拷贝
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。    可以看下,原型对象的成员变量是引用类型Car,确实指向同一地址,没有新城新的引用。

 package lesson1211;

 public class Student implements Cloneable {
private int number;
private Car car; public Student(int number, Car car) { this.car = car;
this.number = number;
} //浅复制
/* (non-Javadoc)
* @see java.lang.Object#clone()
*/
@Override
protected Object clone(){ Student stu = null;
try {
stu = (Student)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return stu;
} /**
* @return the number
*/
public int getNumber() {
return number;
} /**
* @param number the number to set
*/
public void setNumber(int number) {
this.number = number;
} /**
* @return the car
*/
public Car getCar() {
return car;
} /**
* @param car the car to set
*/
public void setCar(Car car) {
this.car = car;
}
} package lesson1211; public class Car { private String name; private int speed; public Car(String name, int speed) {
this.name = name;
this.speed = speed;
} /**
* @return the name
*/
public String getName() {
return name;
} /**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
} /**
* @return the speed
*/
public int getSpeed() {
return speed;
} /**
* @param speed the speed to set
*/
public void setSpeed(int speed) {
this.speed = speed;
} }
package lesson1211; public class TestClone { public static void main(String[] args) {
Car car = new Car(null,20000);
Student stu1 = new Student(3,car);
stu1.setNumber(12345);
stu1.getCar().setName("Baoma"); Student stu2 = (Student)stu1.clone();
System.out.println("stu1: " + stu1.getNumber() + " " + stu1.getCar().getName());
System.out.println("stu2: " + stu2.getNumber() + " " + stu2.getCar().getName()); stu2.setNumber(45678);
stu2.getCar().setName("benchi"); System.out.println("stu1: " + stu1.getNumber() + " " + stu1.getCar().getName());
System.out.println("stu2: " + stu2.getNumber() + " " + stu2.getCar().getName());
}
}

运行结果是:

stu1: 12345 Baoma
stu2: 12345 Baoma
stu1: 12345 benchi   //修改stu2的car类型,stu1里面也会变,说明stu1和stu2里面的引用car指向同一个地址。这就是浅复制。
stu2: 45678 benchi

深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。
简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

Java 深拷贝,浅拷贝

在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。
如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。
序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

同样是上面的例子,我们怎么来通过Object类的clone来实现深克隆,code如下:

 package lesson1211;

 public class Student implements Cloneable {
private int number;
String name;
private Car car; public Student(int number, String name, Car car) { this.car = car;
this.name = name;
this.number = number;
} //浅复制
/* (non-Javadoc)
* @see java.lang.Object#clone()
*/
@Override
protected Object clone(){ Student stu = null;
try {
stu = (Student)super.clone(); //浅复制
stu.car = (Car)car.clone(); //深度复制 } catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return stu;
} /**
* @return the number
*/
public int getNumber() {
return number;
} /**
* @param number the number to set
*/
public void setNumber(int number) {
this.number = number;
} /**
* @return the car
*/
public Car getCar() {
return car;
} /**
* @param car the car to set
*/
public void setCar(Car car) {
this.car = car;
} /**
* @return the name
*/
public String getName() {
return name;
} /**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
} package lesson1211; public class Car implements Cloneable { private String name; private int speed; public Car(String name, int speed) {
this.name = name;
this.speed = speed;
} /**
* @return the name
*/
public String getName() {
return name;
} /**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
} /**
* @return the speed
*/
public int getSpeed() {
return speed;
} /**
* @param speed the speed to set
*/
public void setSpeed(int speed) {
this.speed = speed;
} /* (non-Javadoc)
* @see java.lang.Object#clone()
*/
@Override
public Object clone() {
Car car = null;
try {
car = (Car)super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return car;
}
} package lesson1211; public class TestClone { public static void main(String[] args) {
Car car = new Car(null,20000);
Student stu1 = new Student(3,null, car);
stu1.setNumber(12345);
stu1.setName("zhangsan");
stu1.getCar().setName("Baoma"); Student stu2 = (Student)stu1.clone();
System.out.println("stu1: " + stu1.getNumber() + " " + stu1.getName()+ " " + stu1.getCar().getName());
System.out.println("stu2: " + stu2.getNumber() + " " + stu2.getName()+ " " + stu2.getCar().getName()); stu2.setNumber(45678);
stu2.setName("zhaosi");
stu2.getCar().setName("benchi"); System.out.println("stu1: " + stu1.getNumber() + " " + stu1.getName()+ " " + stu1.getCar().getName());
System.out.println("stu2: " + stu2.getNumber() + " " + stu2.getName()+ " " + stu2.getCar().getName()); }
}

运行结果:

stu1: 12345 zhangsan Baoma
stu2: 12345 zhangsan Baoma
stu1: 12345 zhangsan Baoma
stu2: 45678 zhaosi benchi      //达到了我们想要的深度复制结果。

code主要修改的是Car类,它也必须实现Cloneable接口。   Student在实现clone时,单独在clone 它的引用类型变量。  在student里面添加一个String变量,发现是深克隆。

除了基本数据类型能自动实现深度clone以外,String对象是一个例外,它clone后的表现好象也实现了深度clone,虽然这只是一个假象,但却大大方便了我们的编程。

JDK中StringBuffer类型,关于StringBuffer的说明,StringBuffer没有重载clone()方法,更为严重的是StringBuffer还是一个 final类,这也是说我们也不能用继承的办法间接实现StringBuffer的clone。如果一个类中包含有StringBuffer类型对象或和 StringBuffer相似类的对象,我们有两种选择:要么只能实现影子clone,要么就在类的clone()方法中加一句(假设是 SringBuffer对象,而且变量名仍是p): o.p = new StringBuffer(p.toString()); //原来的是:stu.car = (Car)car.clone();

通过以上我们可以看出在某些情况下,我们可以利用clone方法来实现对象的深度复制,但对于比较复杂的对象(比如对象中包含其他对象,其他对象又包含别的对象…..)这样我们必须进行层层深度clone,每个对象需要实现cloneable接口,比较麻烦,那就继续学习下一个序列化方法。

 利用串行化来做深复制

所谓对象序列化就是将对象的状态转换成字节流,以后可以通过这些值再生成相同状态的对象。

也许你会说,只了解一点点,但从来没有接触过,其实未必如此。RMI、Socket、JMS、EJB你总该用过一种吧,彼此为什么能够传递Java对象,当然都是对象序列化机制的功劳。

第一次使用Java的对象序列化是做某项目,当时要求把几棵非常复杂的树(JTree)及相应的数据保存下来(就是我们常用的保存功能),以便下次运行程序时可以继续上次的操作。

那时XML技术在网上非常的热,而且功能也强大,再加上树的结构本来就和XML存储数据的格式很像。作为一项对新技术比较有兴趣的我当然很想尝试一下。不过经过仔细分析,发现如果采用XML保存数据,后果真是难以想象:哪棵树的哪个节点被展开、展开到第几级、节点当前的属性是什么。真是不知该用A、B、C还是用1、2、3来表示。

还好,发现了Java的对象序列化机制,问题迎刃而解,只需简单的将每棵树的根节点序列化保存到硬盘上,下次再通过反序列化后的根节点就可以轻松的构造出和原来一模一样的树来。

其实保存数据,尤其是复杂数据的保存正是对象序列化的典型应用。最近另一个项目就遇到了需要对非常复杂的数据进行存取,通过使用对象的序列化,问题同样化难为简。

上面这段是摘抄于帖子:https://blog.csdn.net/pony_maggie/article/details/52091588,我自己目前当然没做过。

对象的序列化还有另一个容易被大家忽略的功能就是对象复制(Clone),Java中通过Clone机制可以复制大部分的对象,但是众所周知,Clone有深层Clone和浅层Clone,如果你的对象非常非常复杂,假设有个100层的Collection(夸张了点),如果你想实现深层 Clone,真是不敢想象,如果使用序列化,不会超过10行代码就可以解决。

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());
}

这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,
就需要仔细考察那些不可串行化的对象或属性可否设成transient,从而将之排除在复制过程之外。 上例代码修改如下:
 package lesson1211;

 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 Student implements Serializable{
private int number;
String name;
private Car car; public Student(int number, String name, Car car) { this.car = car;
this.name = name;
this.number = number;
} public Object deepClone() throws IOException, ClassNotFoundException{
ByteArrayOutputStream 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(); } /**
* @return the number
*/
public int getNumber() {
return number;
} /**
* @param number the number to set
*/
public void setNumber(int number) {
this.number = number;
} /**
* @return the car
*/
public Car getCar() {
return car;
} /**
* @param car the car to set
*/
public void setCar(Car car) {
this.car = car;
} /**
* @return the name
*/
public String getName() {
return name;
} /**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
} package lesson1211; import java.io.Serializable; public class Car implements Serializable { private String name; private int speed; public Car(String name, int speed) {
this.name = name;
this.speed = speed;
} /**
* @return the name
*/
public String getName() {
return name;
} /**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
} /**
* @return the speed
*/
public int getSpeed() {
return speed;
} /**
* @param speed the speed to set
*/
public void setSpeed(int speed) {
this.speed = speed;
} } package lesson1211; import java.io.IOException; public class TestClone { public static void main(String[] args) throws IOException, ClassNotFoundException{
Car car = new Car(null,20000);
Student stu1 = new Student(3,null, car);
stu1.setNumber(12345);
stu1.setName("zhangsan");
stu1.getCar().setName("Baoma"); //Student stu2 = (Student)stu1.clone();
Student stu2 = (Student)stu1.deepClone();
System.out.println("stu1: " + stu1.getNumber() + " " + stu1.getName()+ " " + stu1.getCar().getName());
System.out.println("stu2: " + stu2.getNumber() + " " + stu2.getName()+ " " + stu2.getCar().getName()); stu2.setNumber(45678);
stu2.setName("zhaosi");
stu2.getCar().setName("benchi"); System.out.println("stu1: " + stu1.getNumber() + " " + stu1.getName()+ " " + stu1.getCar().getName());
System.out.println("stu2: " + stu2.getNumber() + " " + stu2.getName()+ " " + stu2.getCar().getName()); } }

运行结果:

stu1: 12345 zhangsan Baoma
stu2: 12345 zhangsan Baoma
stu1: 12345 zhangsan Baoma
stu2: 45678 zhaosi benchi
确实也实现了深度复制。看上去感觉比clone还简单

两个问题:1 引用类型类可不可以不实现Serializable?例如Car类不实现Serializable?    不可以,必须实现,否则会抛异常

2:如果引用类型是transient,就不能进行序列化?   尝试将private Car car;改为private transient Car car;  在上面例子中会报错,因为序列化进去的Car是null, 后面getCar.getName()会报空指针的。对于没有实现Serializable接口的,序列化时要主动将其定义为transient,只要后面不在调用这种类引用就可以。

虽然Java的序列化非常简单、强大,但是要用好,还有很多地方需要注意。比如曾经序列化了一个对象,可由于某种原因,该类做了一点点改动,然后重新被编译,那么这时反序列化刚才的对象,将会出现异常。

你可以通过添加serialVersionUID属性来解决这个问题。如果你的类是个单态(Singleton)类,是否允许用户通过序列化机制复制该类,如果不允许你需要谨慎对待该类的实现。

https://blog.csdn.net/pony_maggie/article/details/52091588

https://blog.csdn.net/w410589502/article/details/54985987

https://blog.csdn.net/tounaobun/article/details/8491392

https://www.cnblogs.com/dolphin0520/p/3700693.html

上一篇:001 Java 深拷贝、浅拷贝及Cloneable接口


下一篇:java 深拷贝与浅拷贝机制详解