今天试图去了解一下java基础之对象拷贝与序列化
对象拷贝
对于对象拷贝大家可能有点陌生,这是什么东东?
大家在日常开发中应该是用过的,但是却不知道还有这个好听的名词
那我们怎么去理解这个名词呢?
首先我们要清楚这个概念都包含什么
- 引用拷贝
- 浅拷贝
- 深拷贝
那么我们一点一点的去认识这几个概念~
引用拷贝
下面列出一些简单代码便于大家理解
//我们创建一个对象,此时的对象并没有实现任何接口或者继承其他类,并没有重写equals方法
Student stu1 = new Student();//我们所理解的就是此时创建了对象,堆中为该对象开辟了一处空间
// stu1就是栈中对于该堆中空间的引用
stu1.setName("小哲");
stu1.setAge(18);
stu1.setSex("男");
System.out.println("输出stu1的地址为:>>>>>>>>"+stu1);//Student@eed1f14
//但是此时我们把stu1再赋值给其他另一个对象
Student stu2 = stu1 ;
//那么我们进行下面的操作
System.out.println(stu1==stu2);//true
System.out.println(stu1.equals(stu2));//true
我们可以看到上面的Student stu2 = stu1 操作;就是引用拷贝,下面有示意图便于理解
此时两个对象全部指向的是堆中的同一个地址.
但是这种引用拷贝好不好呢?
下面我们添加一个方法,这样做就是为了改变传进来的对象的属性值
//修改Student对象中的age属性
public static void changeStudent(Student student) {
student.setAge(1);
}
下面我们修改stu2的age的属性值后再进行stu1和stu2的比较
public static void main(String[] args) {
//我们创建一个对象,此时的对象并没有实现任何接口或者继承其他类,并没有重写equals方法
Student stu1 = new Student();//我们所理解的就是此时创建了对象,堆中为该对象开辟了一处空间
// stu1就是栈中对于该堆中空间的引用
stu1.setName("小哲");
stu1.setAge(18);
stu1.setSex("男");
System.out.println("输出stu1的地址为:>>>>>>>>"+stu1);//Student@eed1f14
//但是此时我们把stu1再赋值给其他另一个对象
Student stu2 = stu1 ;
//那么我们进行下面的操作
System.out.println(stu1==stu2);//true
System.out.println(stu1.equals(stu2));//true
//--------------------------我是和谐的分界线--------------------------//
//我们利用修改对象的方法进行操作
changeStudent(stu2);//此时我们改变了stu2中的age属性值
//我们再进行测试一下两者的大小
System.out.println(stu1==stu2);//true
System.out.println(stu1.equals(stu2));//true
}
此时我们依然可以看到还是true,那么我们仔细想想这样做好吗? 我的目的是修改了stu2的age属性值,但是stu1中的age属性值也被破坏了.
那么当我们碰到下面场景的时候,我们看看这种形式的拷贝好用不好用 ?
小王创建了stu1对象并赋予了值,但是此时小王的儿子老王引用拷贝了stu1并更名为stu2利用changeStudent方法修改了age属性.
此时小王想要用stu1,但是却发现age属性被老王给改了,那就很尴尬了,小王是不想用修改了age后的stu1的.
那么此时出现了问题,这种方式的弊端,那么我们如何解决呢?
下面我们引入浅拷贝的概念~
浅拷贝
大家此时看到了浅字 ,可能会猜想是不是还有深拷贝,bingo!~~恭喜你猜对了 !一会我们再讲解深拷贝
我们想要解决上面遗留的问题,我们可以将Student实体类去实现一个接口,也就是Cloneable接口. 还需要重写一个方法 clone方法
下面贴出Student的实体类代码
public class Student implements Cloneable {
private String name;
private int age;
private String sex;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
此时我们发现该实体类已经实现了Cloneable接口.
我们再次调用引用拷贝中的测试代码进行测试
public static void main(String[] args) throws CloneNotSupportedException {
//我们创建一个对象,此时的对象并没有实现任何接口或者继承其他类,并没有重写equals方法
Student stu1 = new Student();//我们所理解的就是此时创建了对象,堆中为该对象开辟了一处空间
// stu1就是栈中对于该堆中空间的引用
stu1.setName("小哲");
stu1.setAge(18);
stu1.setSex("男");
System.out.println("输出stu1的地址为:>>>>>>>>"+stu1);//Student@eed1f14
//但是此时我们把stu1再赋值给其他另一个对象
Student stu2 = (Student) stu1.clone();
//那么我们进行下面的操作
System.out.println(stu1==stu2);//false
System.out.println(stu1.equals(stu2));//false
//--------------------------我是和谐的分界线--------------------------//
//我们利用修改对象的方法进行操作
changeStudent(stu2);//此时我们改变了stu2中的age属性值
//我们再进行测试一下两者的大小
System.out.println(stu1==stu2);//false
System.out.println(stu1.equals(stu2));//false
}
//修改Student对象中的age属性
public static void changeStudent(Student student) {
student.setAge(1);
}
**我们发现我们将Student实现了接口并重写了clone方法,在调用stu1.clone方法赋值给stu2…在进行测试得时候发现stu1与stu2不相等了…即使我们调用下面的changeStudent方法改变了stu2的age属性值但是也不会对stu1有影响…这种方式很好的解决了引用拷贝中的弊端… **
但是这种情况就没有缺点或者弊端吗?
下面我们再次进行测验: 当我们在Student对象中引入一个对象变量,我们再对这个对象变量进行赋值时,会发生什么状况?
我们在Student对象中加入Teacher对象 ,此时的Teacher并未继承任何类或者实现任何接口
private Teacher teacher;
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
Teacher类
package com.test.test.entity;
public class Teacher {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
下面我们查看结果
public static void main(String[] args) throws Exception {
//我们创建一个对象,此时的对象并没有实现任何接口或者继承其他类,并没有重写equals方法
Student stu1 = new Student();//我们所理解的就是此时创建了对象,堆中为该对象开辟了一处空间
// stu1就是栈中对于该堆中空间的引用
stu1.setName("小哲");
stu1.setAge(18);
stu1.setSex("男");
//我们给Student中的Teacher对象进行赋值
Teacher teacher = new Teacher();
teacher.setName("邪神逆天");
teacher.setAge(18);
//给Student赋值Teacher
stu1.setTeacher(teacher);
System.out.println("输出stu1的地址为:>>>>>>>>"+stu1);//Student@eed1f14
//但是此时我们把stu1再赋值给其他另一个对象
Student stu2 = (Student) stu1.clone();
//那么我们进行下面的操作
System.out.println(stu1==stu2);//false
System.out.println(stu1.equals(stu2));//false
//--------------------------我是和谐的分界线--------------------------//
//我们利用修改对象的方法进行操作
changeStudent(stu2);//此时我们改变了stu2中的age属性值
//我们再进行测试一下两者的大小
System.out.println(stu1==stu2);//false
System.out.println(stu1.equals(stu2));//false
//下面取出我们赋予的Teacher的值
System.out.println(stu1.getTeacher() == stu2.getTeacher());//true
System.out.println(stu1.getTeacher().equals(stu2.getTeacher()));//true
}
//修改Student对象中的age属性
public static void changeStudent(Student student) {
student.setAge(1);
}
*我们可以看到上面当把stu1克隆给stu2时,此时stu1和stu2本身还是不相等的也就是说 stu1和stu2是两个地址.但是当我们分别获取stu1和stu2中的teacher对象进行比较时,我们发现此时两者是一致的 ,这就证明此时两个teacher所指向的是一个地址.这也就是浅拷贝的弊端所在,针对于引用类型,此时并不会进行地址的克隆. *
深拷贝
我们如何解决这种情况呢? 下面需要引入了深拷贝件进行讲解
概念:
首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
深拷贝模型如图所示:可以看到所有的成员变量都进行了复制
深拷贝模型图
有两种方式可以实现深拷贝:
- 通过重写clone方法来实现深拷贝
与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。
那我们把上面的teacher实现cloneable接口并重写clone方法再进行测试
重写Student中的clone方法:
public Object clone() throws CloneNotSupportedException {
Object obj=null;
//调用Object类的clone方法——浅拷贝
try {
obj= super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//调用Age类的clone方法进行深拷贝
//先将obj转化为学生类实例
Student stu=(Student)obj;
//学生类实例的Age对象属性,调用其clone方法进行拷贝
stu.teacher=(Teacher) stu.getTeacher().clone();
return obj;
}
我们再次进行测试
public static void main(String[] args) throws Exception {
//我们创建一个对象,此时的对象并没有实现任何接口或者继承其他类,并没有重写equals方法
Student stu1 = new Student();//我们所理解的就是此时创建了对象,堆中为该对象开辟了一处空间
// stu1就是栈中对于该堆中空间的引用
stu1.setName("小哲");
stu1.setAge(18);
stu1.setSex("男");
//我们给Student中的Teacher对象进行赋值
Teacher teacher = new Teacher();
teacher.setName("邪神逆天");
teacher.setAge(18);
//给Student赋值Teacher
stu1.setTeacher(teacher);
System.out.println("输出stu1的地址为:>>>>>>>>"+stu1);//Student@eed1f14
//但是此时我们把stu1再赋值给其他另一个对象
Student stu2 = (Student) stu1.clone();
//那么我们进行下面的操作
System.out.println(stu1==stu2);//false
System.out.println(stu1.equals(stu2));//false
//--------------------------我是和谐的分界线--------------------------//
//我们利用修改对象的方法进行操作
changeStudent(stu2);//此时我们改变了stu2中的age属性值
//我们再进行测试一下两者的大小
System.out.println(stu1==stu2);//false
System.out.println(stu1.equals(stu2));//false
//下面取出我们赋予的Teacher的值
System.out.println(stu1.getTeacher() == stu2.getTeacher());//false
System.out.println(stu1.getTeacher().equals(stu2.getTeacher()));//false
}
//修改Student对象中的age属性
public static void changeStudent(Student student) {
student.setAge(1);
}
*可以发现上面的对stu1和stu2中的teacher进行比较时发现其中两者是不相同的.这就实现了深拷贝 *
- 通过对象序列化实现深拷贝
虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。
将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。
此时我们取消Student和Teacher中的cloneable接口,去掉其中的clone方法,并两者全部实现序列化接口Serializable
public static void main(String[] args) throws Exception {
Teacher teacher = new Teacher();
teacher.setAge(19);
Student stu1=new Student();
stu1.setName("邪神逆天");
stu1.setAge(18);
stu1.setSex("男");
stu1.setTeacher(teacher);
//通过序列化方法实现深拷贝
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(stu1);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Student stu2=(Student)ois.readObject();
System.out.println(stu1.toString());
System.out.println(stu2.toString());
System.out.println(stu1==stu2);
System.out.println(stu1.equals(stu2));
System.out.println(stu1.getTeacher()==stu2.getTeacher());
System.out.println();
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变teacher这个引用类型的成员变量的值
teacher.setAge(99);
stu1.setAge(216);
System.out.println(stu1.toString());
System.out.println(stu2.toString());
System.out.println(stu1==stu2);
System.out.println(stu1.equals(stu2));
System.out.println(stu1.getTeacher()==stu2.getTeacher());
}
我们可以看到输出的结果:
Student{name='邪神逆天', age=18, sex='男', teacher=Teacher{name='null', age=19}}
Student{name='邪神逆天', age=18, sex='男', teacher=Teacher{name='null', age=19}}
false
false
false
Student{name='大傻子', age=216, sex='男', teacher=Teacher{name='null', age=99}}
Student{name='邪神逆天', age=18, sex='男', teacher=Teacher{name='null', age=19}}
false
false
false
所以此时也成功了. 分别比较stu1和stu2后可以发现两者是不相等的. 深拷贝成功,这种方式比较容易,比第一种简单多了
序列化
那么序列化是一种什么东西呢?
序列化:把对象转化为可传输的字节序列过程为对象的序列化。
反序列化:把字节序列还原为对象的过程。
那么我们为什么进行序列化呢?
- 我们首先要明白序列化是一种处理***对象流***的机制
- 对象流:就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。
- 序列化是为了解决在对对象流进行读写操作时所引发的问题。
- 序列化的实现:将需要被序列化的类实现Serializable接口(标记接口),该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象;接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
但是我们什么时候使用序列化呢?
- 对象序列化可以实现分布式对象。
这里需要说到远程调用协议了,实现了序列化那么访问其他主机中的对象就跟访问本地对象一样简单 - java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。
可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深拷贝",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。 - 序列化可以将内存中的类写入文件或数据库中。
将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。 - 对象、文件、数据,有许多不同的格式,很难统一传输和保存。
序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件
所以经过上面的总结,序列化主要是可以实现两个目的
- 把对象的字节永久的保存在本地硬盘中,通常存放在一个文件中
- 在网络上传输对象的字节码文件
有一个着重需要注意的分别序列化的一个标示serialVersionUID,也叫序列化id
- Java的序列化机制是通过运行时判断类的序列化ID(serialVersionUID)来判定版本的一致性。
- 在反序列化时,java虚拟机会通过二进制流中的serialVersionUID与本地的对应的实体类进行比较,如果相同就认为是一致的,可以进行反序列化,正确获得信息,否则抛出序列化版本不一致的异常。
那么被什么修饰的属性不能实现序列化呢?
- static修饰的属性不能实现序列化
- Transient修饰的属性不能被序列化