Java对象深复制、浅复制

我们在编码过程经常会碰到将一个对象传递给另一个对象,java中对于基本型变量采用的是值传递,而对于对象比如bean传递时采用的引用传递也就是地址传递,而很多时候对于对象传递我们也希望能够象值传递一样,使得传递之前和之后有不同的内存地址,在这种情况下我们一般采用以下两种情况。

浅复制与深复制概念

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

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

Java的clone()方法

(1)clone方法将对象复制了一份并返回给调用者。一般而言,clone()方法满足:

①对任何的对象x,都有x.clone() !=x//克隆对象与原对象不是同一个对象;

②对任何的对象x,都有x.clone().getClass()= =x.getClass()//克隆对象与原对象的类型一样 ;

③如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立;

(2)Java中对象的克隆:

①为了获取对象的一份拷贝,我们可以利用Object类的clone()方法。

②在派生类中覆盖基类的clone()方法,并声明为public。

③在派生类的clone()方法中,调用super.clone()。

④在派生类中实现Cloneable接口。

package com.king.cloneable;

/**

* 浅复制

*/

public class ShallowStudent implements Cloneable {

private String name;

private int age;

ShallowStudent(String name, int age) {

this.name = name;

this.age = age;

}

public Object clone() {

ShallowStudent o = null;

try {

// Object中的clone()识别出你要复制的是哪一个对象。

o = (ShallowStudent) super.clone();

} catch (CloneNotSupportedException e) {

System.out.println(e.toString());

}

return o;

}

public static void main(String[] args) {

ShallowStudent s1 = new ShallowStudent("zhangsan", 18);

ShallowStudent s2 = (ShallowStudent) s1.clone();

s2.name = "lisi";

s2.age = 20;

//修改学生2后,不影响学生1的值。

System.out.println("name=" + s1.name + "," + "age=" + s1.age);

System.out.println("name=" + s2.name + "," + "age=" + s2.age);

}

}

Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。 JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用 new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。

上面代码中有三个值得注意的地方,一是希望能实现clone功能的CloneClass类实现了Cloneable接口,这个接口属于java.lang包, java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable。另一个值得请注意的是重载了clone()方法。最后在clone()方法中调用了super.clone(),这也意味着无论clone类的继承结构是什么样的,super.clone()直接或间接调用了java.lang.Object类的clone()方法。

下面再详细的解释一下这几点:

应该说第三点是最重要的,仔细观察一下Object类的clone()一个native方法,native方法的效率一般来说都是远高于 java中的非native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能。对于第二点,也要观察Object类中的clone()还是一个protected属性的方法。这也意味着如果要应用 clone()方法,必须继承Object类,在Java中所有的类是缺省继承Object类的,也就不用关心这点了。然后重载clone()方法。还有一点要考虑的是为了让其它类能调用这个clone类的clone()方法,重载之后要把clone()方法的属性设置为public。

那么clone类为什么还要实现Cloneable接口呢?稍微注意一下,Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的 clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException异常。

说明:

①为什么我们在派生类中覆盖Object的clone()方法时,一定要调用super.clone()呢?在运行时刻,Object中的clone()识别出你要复制的是哪一个对象,然后为此对象分配空间,并进行对象的复制,将原始对象的内容一一复制到新对象的存储空间中。

②继承自java.lang.Object类的clone()方法是浅复制。以下代码可以证明之。

package com.king.cloneable;

/**

* 浅复制2

*/

public class ShallowStudent2 implements Cloneable {

String name;// 常量对象。

int age;

Professor p;// 学生1和学生2的引用值都是一样的。

ShallowStudent2(String name, int age, Professor p) {

this.name = name;

this.age = age;

this.p = p;

}

public Object clone() {

ShallowStudent2 o = null;

try {

o = (ShallowStudent2) super.clone();

} catch (CloneNotSupportedException e) {

System.out.println(e.toString());

}

return o;

}

public static void main(String[] args) {

Professor p = new Professor("wangwu", 50);

ShallowStudent2 s1 = new ShallowStudent2("zhangsan", 18, p);

ShallowStudent2 s2 = (ShallowStudent2) s1.clone();

s2.p.name = "lisi";

s2.p.age = 30;

System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);

System.out.println("name=" + s2.p.name + "," + "age=" + s2.p.age);

//输出结果学生1和2的教授成为lisi,age为30。

}

}

class Professor {

String name;

int age;

Professor(String name, int age) {

this.name = name;

this.age = age;

}

}

从中可以看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为”影子clone”。

那应该如何实现深层次的克隆,即修改s2的教授不会影响s1的教授?代码改进如下。 改进使学生1的Professor不改变(深层次的克隆):

package com.king.cloneable;

/**

* 深复制

*/

public class DeepStudent implements Cloneable {

String name;// 常量对象。

int age;

DeepProfessor p;// 学生1和学生2的引用值都是一样的。

DeepStudent(String name, int age, DeepProfessor p) {

this.name = name;

this.age = age;

this.p = p;

}

public Object clone() {

DeepStudent o = null;

try {

o = (DeepStudent) super.clone();

//对引用的对象也进行复制

o.p = (DeepProfessor) p.clone();

} catch (CloneNotSupportedException e) {

System.out.println(e.toString());

}

return o;

}

public static void main(String[] args) {

DeepProfessor p = new DeepProfessor("wangwu", 50);

DeepStudent s1 = new DeepStudent("zhangsan", 18, p);

DeepStudent s2 = (DeepStudent) s1.clone();

s2.p.name = "lisi";

s2.p.age = 30;

System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);

System.out.println("name=" + s2.p.name + "," + "age=" + s2.p.age);

//输出结果学生1和2的教授成为lisi,age为30。

}

}

class DeepProfessor implements Cloneable {

String name;

int age;

DeepProfessor(String name, int age) {

this.name = name;

this.age = age;

}

public Object clone() {

DeepProfessor o = null;

try {

o = (DeepProfessor)super.clone();

} catch(CloneNotSupportedException e) {

System.out.println(e.toString());

}

return o;

}

}

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

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

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

利用串行化来做深复制

所谓对象序列化就是将对象的状态转换成字节流,以后可以通过这些值再生成相同状态的对象。这个过程也可以通过网络实现,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新?装配?。是不是很神奇。

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

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

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

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

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

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

主要是为了避免重写比较复杂对象的深复制的clone()方法,也可以程序实现断点续传等等功能。把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做 “解冻”或者“回鲜(depicking)”过程。

应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。

在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。

如下为深复制源代码:

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 com.king.cloneable;

import java.io.*;

/**

* 通过串行化实现深复制

*/

class Teacher implements Serializable {

String name;

int age;

public Teacher(String name, int age) {

this.name = name;

this.age = age;

}

}

public class DeepStudent2 implements Serializable {

String name;//常量对象

int age;

Teacher t;//学生1和学生2的引用值都是一样的。

public DeepStudent2(String name, int age, Teacher t) {

this.name = name;

this.age = age;

this.t = t;

}

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

}

public static void main(String[] args) throws IOException, ClassNotFoundException {

Teacher t = new Teacher("tangliang", 30);

DeepStudent2 s1 = new DeepStudent2("zhangsan", 18, t);

DeepStudent2 s2 = (DeepStudent2) s1.deepClone();

s2.t.name = "tony";

s2.t.age = 40;

//学生1的老师不改变

System.out.println("name=" + s1.t.name + "," + "age=" + s1.t.age);

}

}

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

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

时间: 2024-10-13 15:40:42

Java对象深复制、浅复制的相关文章

OpenCv学习笔记(四)--Mat基本图像容器Mat对象信息头,矩阵体的创建,深复制,浅复制详解

1--我们知道Mat是一个图像容器类,这个数据结构由两部分组成: 1--矩阵头--即class Mat类所实例化的类对象所开辟的空间里面存储的数据---就是这个矩阵的信息,当我们以 Mat object;这样声明类对象的时候,也仅仅是创建了一个Mat的信息头,并没有创建矩阵体,也就是说,我们并 没有给将要存储的图像开辟相应的空间 2--矩阵头--包含: 1--矩阵的尺寸----比如---class Mat这个类中的----数据成员rows,cols---就可以指定图像的尺寸 2--存储方法---

深复制 浅复制

深复制:不仅复制对象 还复制对象关联的对象 浅复制:只是复制对象本身 不复制对象关联的对象 // // main.m // 对象的复制 // // Created by MAC on 15/12/20. // Copyright © 2015年 MAC. All rights reserved. // #import <Foundation/Foundation.h> #import "Person.h" int main(int argc, const char * ar

简单理解php深复制浅复制问题

其实接触深复制浅复制是通过学习c++了解到的,比如c++很好用的模板,php是不允许方法模板和类模板 一个简单的例子,如果不是很了解php 的取地址符&,可以去看下官方文档,php的&就是别名 class zx{ public $r ='44444444<br>'; public function __construct() { } public function ccccc() { echo $this->r; } } $rr = new zx(); //$ee = $

【C/C++】复制构造函数、深复制浅复制

常见问题 Q1. 下面代码的输出结果是( )? 1 #include <iostream> 2 using namespace std; 3 4 class MyClass 5 { 6 public: 7 MyClass(int n) { number = n; } 8 MyClass(const MyClass &other) 9 { 10 number = other.number; 11 cout << "a "; 12 } 13 private:

iOS学习(二)之深复制&amp;浅复制

1.无论是深复制还是浅复制,被复制的对象类型是不变的.此对象类型具有什么功能就具有什么功能,不会因为自行修改了返回对象的指针类型而改变. 比如: 这里的str和str1的值和指针地址完全一样,因为是不可变类型使用了copy,什么都不变.为什么NSNumber类型的指针可以指向NSSring类型copy出的对象呢?因为copy返回类型为id万能指针类型.如果返回的是instancetype就会警告. 记住:对于不可变的临时变量使用浅复制copy没什么用,地址和值都一样. 2.怎么理解最后一句话?

复制构造函数——深复制 浅复制

隐含的复制构造函数并不总是适用的,因为它完成的只是浅复制. 对象的浅复制 1 #include<iostream> 2 #include<cassert> 3 using namespace std; 4 class Point{ 5 public: 6 Point() :x(0), y(0)//默认构造函数 7 { 8 cout << "constructor1" << endl; 9 } 10 Point(int x, int y)

Python深复制浅复制or深拷贝浅拷贝

1. copy.copy 浅拷贝 只拷贝父对象,不会拷贝对象的内部的子对象.(比深拷贝更加节省内存) 2. copy.deepcopy 深拷贝 拷贝对象及其子对象 小例子: import copy a = [1, 2, 3, 4, ['a', 'b', 'c']] b = a c = copy.copy(a) d = copy.deepcopy(a) print(id(a))#2552169009288 print(id(b))#2552169009288 print(id(c))#255216

JavaScript中对象的浅复制和深复制

在JavaScript中,如果要复制一个变量我们应该怎么做呢?下面这种做法是最简单的一种形式: //把a复制给b let a = 12; let b = a; 这种复制方法只能适用于基本类型,如果a是对象怎么办呢?我们先来看看上面的代码在内存中做了什么事: 声明了变量a = 12,栈内存会分配一块区域来存储,如上图所示.把a赋给b,会在栈中重新开辟一块区域来存储b,并且b的值就是a的值. 假如a是对象,内存做了什么事呢?来看下面的例子: let a = {}; let b = a; 如图所示,对

原型模式——浅复制与深复制

原型模式涉及一个浅复制和深复制的概念.原型模式可以简单理解为“复制”,但这个复制不是代码的复制.对同一个类,我们可以实例化new三次来“复制”,但如果在初始化的时候构造函数的执行很长,多次实例化就显得效率很低效了.那我们能否只实例化一次,然后“复制”呢? Test test1 = new Test(); Test test2 = test1; Test test3 = test1; 这样写吗?注意这是引用的复制,这实际上还是只有test1一个实例,test2.test3只是复制了其引用而已,如果