java 深拷贝 序列化_Java深拷贝与序列化

本文介绍了Java中对象拷贝的两种方式:浅拷贝和深拷贝。浅拷贝仅复制对象的基本类型属性,而深拷贝则包括对象内的引用类型属性。通过实现Cloneable接口的clone方法可以实现浅拷贝,但需要手动处理引用类型的深拷贝。另一种深拷贝方法是利用Java的序列化和反序列化,简单且能实现全对象的深拷贝,但需注意序列化标识serialVersionUID的使用。文章还提醒了序列化中static和transient类型的变量不会被序列化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对基本类型的变量进行拷贝非常简单,直接赋值给另外一个对象即可:

1 int b = 50;2 int a = b; //基本类型赋值

对于引用类型的变量(例如 String),情况稍微复杂一些,因为直接等号赋值只是复制了一份引用,而复制前后的两个引用指向的是内存中的同一个对象。

要想实现引用类型的拷贝,可以通过实现 Cloneable 接口,并覆盖其中的 clone 方法来实现。

看一个例子,首先定义一个待拷贝的 Student 类,为简单起见,只设置了一个 name 属性

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 class Student implementsCloneable{2 privateString name;3

4 publicString getName() {5 returnname;6 }7

8 public voidsetName(String name) {9 this.name =name;10 }11

12 @Override13 publicObject clone(){14 Student s = null;15 try{16 s = (Student)super.clone();17 }catch(Exception e){18 e.printStackTrace();19 }20 returns;21 }22 }

Student

可以看到,在 clone 方法里实际上是调用了 super.clone() 方法

接下来对这个类进行复制,只需要调用 clone 方法即可:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 public voiddeepCopy(){2 Student s1 = newStudent();3 s1.setName("zhang");4

5 Student s2 =(Student) s1.clone();6 s1.setName("wang");7 System.out.println(s1.getName());8 System.out.println(s2.getName());9 }

deepCopy

输出结果为:

wangzhang

由于s1修改了name属性值,输出的结果中s1和s2的name属性并不相同,说明这两个引用指向了不同的 Student 对象,实现了对象拷贝。

但是,如果在Student中间添加一个引用对象,那么这种拷贝方式就会产生问题。

为了说明问题,定义一个Car类,同样只有一个name属性:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 classCar{2 privateString name;3

4 publicString getName() {5 returnname;6 }7

8 public voidsetName(String name) {9 this.name =name;10 }11 }

Car类定义

对 Student 类进行修改,添加一个 Car 类型的属性(略去这部分代码),在 deepCopy 方法里面对 Car 的 name 值进行修改,如下:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 public voiddeepCopy(){2 Student s1 = newStudent();3 s1.setName("zhang");4 Car car = newCar();5 car.setName("Audi");6 s1.setCar(car);7

8 Student s2 =(Student) s1.clone();9 s1.setName("wang");10 car.setName("BMW");11 System.out.println(s1.getName());12 System.out.println(s2.getName());13 System.out.println(s1.getCar().getName());14 System.out.println(s2.getCar().getName());15 }

修改后的deepCopy

修改后的输出结果如下:

wangzhangBMWBMW

我们发现,对于 Car 类型的复制出现了问题,s1 和 s2 的Car属性的 name 值是相同的,都是修改后的 BMW,可以推测 s1 和 s2 的 Car 属性指向了内存中的同一个对象。通过s1.getCar() == s2.getCar() 进行验证,输出为 true,说明确实引用了同一个对象。

出现问题的原因是,上面的方法是浅拷贝方法。所谓浅拷贝,是指拷贝对象的时候只是对其中的基本类型属性进行复制,而并不拷贝对象中的引用属性。而我们想要实现的效果是连同 Student 中的引用类型属性一起复制,这就是深拷贝。深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大

为了解决这个问题,一种可行的方式是让 Car 类也实现 Cloneable 接口,并覆盖 clone 方法,在 Student 类的 clone 方法里加上一行代码:

this.car = (Car)car.clone()

这样的确能够解决 Car 没有复制的问题,然而如果 Student 中有多个引用类型属性,这些对象有可能也会有其他的引用类型属性,那么上面这种做法就要去所有的相关类都要实现 Cloneable 接口,并覆盖 clone 方法,不仅麻烦,而且非常不利于后期维护和扩展。

一种比较优雅的做法是利用 Java 的序列化和反序列化实现深拷贝。序列化是指将对象转换成字节序列的过程,反序列化是指将字节序列还原成对象的过程。一般在对象持久化保持或者进行网络传输的时候会用到序列化。【需要注意的是 static 和 transient 类型的变量不会被序列化】

利用序列化和反序列化进行深拷贝比较简单,只需要实现 Serializable 接口就行。我们对Student类就行修改,如下:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 class Student implementsSerializable{2

3 //private static final long serialVersionUID = 1L;

4

5 privateString name;6 privateCar car;7

8 publicCar getCar() {9 returncar;10 }11

12 public voidsetCar(Car car) {13 this.car =car;14 }15

16 publicString getName() {17 returnname;18 }19

20 public voidsetName(String name) {21 this.name =name;22 }23 }

修改后的Student

这里暂时忽略其中的 serialVersionUID 属性,让Car类也同样实现 Serializable 接口,之后定义一个深拷贝的方法:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 public voiddeepCopyWithSerialize(){2 Student s1 = newStudent();3 s1.setName("zhang111");4 Car car = newCar();5 car.setName("Audi");6 s1.setCar(car);7

8 ObjectOutputStream oo;9 try{10 oo = new ObjectOutputStream (new FileOutputStream("a.txt"));11 oo.writeObject(s1);12 oo.close();13

14 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));15 Student s2 =(Teacher) ois.readObject();16

17 s1.setName("wahah");18 car.setName("BMW");19 System.out.println(s1.getName());20 System.out.println(s2.getName());21 System.out.println(s1.getCar().getName());22 System.out.println(s2.getCar().getName());23 } catch(IOException e) {24 //TODO Auto-generated catch block

25 e.printStackTrace();26 } catch(ClassNotFoundException e) {27 //TODO Auto-generated catch block

28 e.printStackTrace();29 }30

31 }

deepCopyWithSerialize

输出结果为:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

wahah

zhang111

BMW

Audi

输出结果

可以看出,成功实现了对象的深拷贝。这里选择了利用文件来保存序列化的对象,也可以选择其他的形式,例如 ByteArrayOutputStream

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 ByteArrayOutputStream baos = newByteArrayOutputStream();2 ObjectOutputStream oos = newObjectOutputStream(baos);3 oos.writeObject(s1);4

5 //从流中读出对象

6 ByteArrayInputStream bais = newByteArrayInputStream(baos.toByteArray());7 ObjectInputStream ois = newObjectInputStream(bais)8 Student s2 = ois.readObject();

ByteArrayOutputStream序列化

接下来解释一下刚才忽略的 serialVersionUID,根据名字知道这是一个与对象的状态有关的变量,如果代码中没有定义这样的变量,那么在运行的时候会按照一定的方式自动生成,在反序列化的时候会对这个值进行判断,如果两个值不相等,会抛出 InvalidClassException 。由于计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,一般建议在序列化的时候主动提供这个参数。

【总结】

① Cloneable 接口的 clone 方法默认是浅拷贝,需要自行覆盖才能实现深拷贝。

② 使用 Serializable 序列化的方式实现深拷贝比较简单,但是需要注意定义 serialVersionUID 的值,并且 static 和 transient 类型的变量不会被序列化。

【参考资料】

本文的内容主要参考了以下的博客,在此表示感谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值