Java深拷贝与序列化
对基本类型的变量进行拷贝非常简单,直接赋值给另外一个对象即可:
int b = 50;
int a = b; // 基本类型赋值
对于引用类型的变量(例如 String),情况稍微复杂一些,因为直接等号赋值只是复制了一份引用,而复制前后的两个引用指向的是内存中的同一个对象。
要想实现引用类型的拷贝,可以通过实现 Cloneable 接口,并覆盖其中的 clone 方法来实现。
看一个例子,首先定义一个待拷贝的 Student 类,为简单起见,只设置了一个 name 属性
class Student implements Cloneable{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Object clone(){
Student s = null;
try{
s = (Student)super.clone();
}catch(Exception e){
e.printStackTrace();
}
return s;
}
}
Student
可以看到,在 clone 方法里实际上是调用了 super.clone() 方法
接下来对这个类进行复制,只需要调用 clone 方法即可:
public void deepCopy(){
Student s1 = new Student();
s1.setName("zhang");
Student s2 = (Student) s1.clone();
s1.setName("wang");
System.out.println(s1.getName());
System.out.println(s2.getName());
}
deepCopy
输出结果为:
wang
zhang
由于s1修改了name属性值,输出的结果中s1和s2的name属性并不相同,说明这两个引用指向了不同的 Student 对象,实现了对象拷贝。
但是,如果在Student中间添加一个引用对象,那么这种拷贝方式就会产生问题。
为了说明问题,定义一个Car类,同样只有一个name属性:
class Car{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Car类定义
对 Student 类进行修改,添加一个 Car 类型的属性(略去这部分代码),在 deepCopy 方法里面对 Car 的 name 值进行修改,如下:
public void deepCopy(){
Student s1 = new Student();
s1.setName("zhang");
Car car = new Car();
car.setName("Audi");
s1.setCar(car);
Student s2 = (Student) s1.clone();
s1.setName("wang");
car.setName("BMW");
System.out.println(s1.getName());
System.out.println(s2.getName());
System.out.println(s1.getCar().getName());
System.out.println(s2.getCar().getName());
}
修改后的deepCopy
修改后的输出结果如下:
wang
zhang
BMW
BMW
我们发现,对于 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类就行修改,如下:
class Student implements Serializable{
//private static final long serialVersionUID = 1L;
private String name;
private Car car;
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
修改后的Student
这里暂时忽略其中的 serialVersionUID 属性,让Car类也同样实现 Serializable 接口,之后定义一个深拷贝的方法:
public void deepCopyWithSerialize(){
Student s1 = new Student();
s1.setName("zhang111");
Car car = new Car();
car.setName("Audi");
s1.setCar(car);
ObjectOutputStream oo;
try {
oo = new ObjectOutputStream (new FileOutputStream("a.txt"));
oo.writeObject(s1);
oo.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
Student s2 = (Teacher) ois.readObject();
s1.setName("wahah");
car.setName("BMW");
System.out.println(s1.getName());
System.out.println(s2.getName());
System.out.println(s1.getCar().getName());
System.out.println(s2.getCar().getName());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
deepCopyWithSerialize
输出结果为:
wahah
zhang111
BMW
Audi
输出结果
可以看出,成功实现了对象的深拷贝。这里选择了利用文件来保存序列化的对象,也可以选择其他的形式,例如 ByteArrayOutputStream
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(s1); // 从流中读出对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais)
Student s2 = ois.readObject();
ByteArrayOutputStream序列化
接下来解释一下刚才忽略的 serialVersionUID,根据名字知道这是一个与对象的状态有关的变量,如果代码中没有定义这样的变量,那么在运行的时候会按照一定的方式自动生成,在反序列化的时候会对这个值进行判断,如果两个值不相等,会抛出 InvalidClassException 。由于计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性,一般建议在序列化的时候主动提供这个参数。
【总结】
① Cloneable 接口的 clone 方法默认是浅拷贝,需要自行覆盖才能实现深拷贝。
② 使用 Serializable 序列化的方式实现深拷贝比较简单,但是需要注意定义 serialVersionUID 的值,并且 static 和 transient 类型的变量不会被序列化。
【参考资料】
本文的内容主要参考了以下的博客,在此表示感谢
请叫我大师兄-Java 之 Serializable 序列化和反序列化的概念,作用的通俗易懂的解释
Java序列化之排除被序列化字段(transient/静态变量)
Java深拷贝与序列化的更多相关文章
- Java 深拷贝和浅拷贝 利用序列化实现深拷贝
Java 深拷贝和浅拷贝 转自:http://www.cnblogs.com/mengdd/archive/2013/02/20/2917971.html 深拷贝(deep clone)与浅拷贝(sh ...
- 【Java基础】序列化与反序列化深入分析
一.前言 复习Java基础知识点的序列化与反序列化过程,整理了如下学习笔记. 二.为什么需要序列化与反序列化 程序运行时,只要需要,对象可以一直存在,并且我们可以随时访问对象的一些状态信息,如果程序终 ...
- 一种c#深拷贝方式完胜java深拷贝(实现上的对比)
楼主是一名asp.net攻城狮,最近经常跑java组客串帮忙开发,所以最近对java的一些基础知识特别上心.却遇到需要将一个对象深拷贝出来做其他事情,而原对象保持原有状态的情况.(实在是不想自己new ...
- java 深拷贝与浅拷贝机制详解
概要: 在Java中,拷贝分为深拷贝和浅拷贝两种.java在公共超类Object中实现了一种叫做clone的方法,这种方法clone出来的新对象为浅拷贝,而通过自己定义的clone方法为深拷贝. (一 ...
- 对Java Serializable(序列化)的理解和总结
我对Java Serializable(序列化)的理解和总结 博客分类: Java技术 JavaOSSocketCC++ 1.序列化是干什么的? 简单说就是为了保存在内存中的各种对象的状 ...
- java.io.Serializable 序列化问题
java.io.Serializable 序列化问题 Person.java package a.b.c; public class Person implements java.io.Seriali ...
- java对象的序列化与反序列化使用
1.Java序列化与反序列化 Java序列化是指把Java对象转换为字节序列的过程:而Java反序列化是指把字节序列恢复为Java对象的过程. 2.为什么需要序列化与反序列化 我们知道,当两个进程进 ...
- Java transient关键字序列化时使用小记
1. transient的作用及使用方法 我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过 ...
- 【Java】Java原生的序列化和反序列化
写一个Java原生的序列化和反序列化的DEMO. 需序列化的类: package com.nicchagil.nativeserialize; import java.io.Serializable; ...
随机推荐
- 从源码看Flask框架配置管理
1 引言 Flask作为Python语言web开发的三大顶梁柱框架之一,对于配置的管理当然必不可少.一个应用从开发到测试到最后的产品发布,往往都需要多种不同的配置,例如是否开启调试模式.使用哪个数据库 ...
- python 之 前端开发(基本选择器、组合选择器、 交集与并集选择器、序列选择器、属性选择器、伪类选择器、伪元素选择器)
11.3 css 11.31 基本选择器 11.311 id选择器 根据指定的id名称,在当前界面中找到对应的唯一一个的标签,然后设置属性 <!DOCTYPE html> <html ...
- centos7之Python3.74安装
安装版本:Python3.74 系统版本:centos7 系统默认安装Python2.7,保留. 安装/usr/bin/Python3 安装需要root权限. 安装Python3的准备工作: 1.安装 ...
- python(自用手册)三
第三章 基础 3.1编码初识 ascii 256字母没有中文 一个字节 8位 gbk 中国 中文2字节 16位 英文1字节8位 unicode 万国码 前期 2字节 8位 后期变成4个字节 32位 u ...
- LoRaWAN调试踩坑心得(二)
先说两句 抱歉,由于工作原因和个人原因,中间停更了这么久.接下来,本人会继续往下更,内容包括但不仅限于LoRa.文章还是会按照个人的习惯,坚持原创,一是作为自己的笔记,二是和广大工程师分享交流. Lo ...
- Integer 使用==判断127和超过128的数据的区别
Integer封装类型字数据当超过一定长度后,若使用==来判断数否相等,那么判断的结果是false; Integer的范围是超过128就是false. 对于所有封装类而言,建议使用equals来进行判 ...
- MyBatis 返回 Map 字段丢失问题
问题现象 执行存储过程返回 Map 集合数据,发现有字段丢失情况,仔细研究发现丢失的字段值都为 NULL. 解决办法1: 在查询 SQL 语句中增加 NULL 判断函数 MSSQL: isnull(字 ...
- 做「容量预估」可没有true和false
如果第二次看到我的文章,欢迎右侧扫码订阅我哟~
- unity之游戏特效
一.运动轨迹 运动轨迹常常用于表现武器的挥舞效果,在提高速度感的同时又能让玩家看清楚招式动作,所以是常见的一种格斗特效. Unity中可以直接使用TrailRender来实现该效果. 二.运动模糊 运 ...
- 2013-2014 ACM-ICPC Pacific Northwest Regional Contest B.Bones’s Battery
题意略. 思路: 这个题目求的是第一个可行解,由此想到用二分试探的方式来解决. 现在讲讲怎么验证该解是否合理: 先用floyd求出两两之间的最短距离. dp[ i ][ j ]表示,i 到 j 至少要 ...