Java序列化的学习
首先说明,不想要看前面的介绍的,可直接跳到之后的实现阶段,以免影响您的阅读体验。
Java序列化的相关介绍
序列化的由来与意义
那么在学习序列化之前我们需要明确什么是序列化,我们可以到网上去搜啊,各种各样的,也有的总结的很完美啊!这里我就直接说了,我们经常在内存中创建一些对象,那这些对象被创建之后,被垃圾回收之后其实也就没了,或者我们去创建一些静态的的变量,我们想在程序的开始到结束一直的去使用它,那我们的程序不可避免的会关闭,那关闭了程序,变量也就消失了。那问题来了,我们程序员有需求,说我们能不能把我们创建的对象就创建到我们的文件里,下次运行的时候就把对象拿过来。那时候大家都在思考这个问题,那就在这个时候,就有人提出来一个概念,那就是序列化的概念。那所谓的序列化就是将我们程序中的对象直接以文件形式存储起来。 这种存储是按照我们的对象在内存中的存储序列来存储的。是对象在内存中存储的字节序列。那我们光存储起来也不行啊,我们要用怎么办,那反序列化技术就是把我们的对象再从文件中读取出来。
Java序列化概念
那我们在明白了序列化之后呢,对于Java序列化概念的理解就易如反掌了。那在我们Java中,认为 Java序列化是指把Java对象转换为字节序列的过程,Java反序列化是指把字节序列恢复为Java对象的过程。
意义
那我们学习这个知识有什么用呢?其实在前些年,在涉及到IO的时候我们都是会吧Java序列化和反序列化当成重点去讲。那为什么要讲是前几年呢?因为它太优秀了,在97年出现之后,大量的程序员都在去使用。就我们这几年比较流行的分布式微服务。这里首先引入一个概念啊,就是说一个服务器支撑不了,假如说10000个用户进行操作,那怎么办,我们就在每个地区都给一个服务器,那在这个地区的用户就去找本地区的服务器,这样就把用户分成区域单独服务了,那服务器的使用压力就会小很多。当然,如果大家对此感兴趣可以自行搜索,我就不再推荐了,探寻知识的感觉还是很美好的。好,言归正传,话续前言,那我们之前分区域的服务器说白了都是服务用户,那这里同样的操作,服务器之间就会有交流,也就是信息共享。那怎么实现这个交流,我们就是通过Java序列化和反序列化。那其实到目前,有很多常见的框架都还是在使用Java序列化与反序列化。比如说:SSM,SSH,还有就是绝大多数的分布式框架,如阿里的Dubbo都是在使用序列化技术。但是,小故事来了,在18年的时候,Java官方发了一个公告说,这个技术还是不要用为好,为什么不用呢?Java官方说,从1997年推出这个技术,大量的程序员都在使用,那也就导致了之后Java官方在调试Bug的时候发现,在这些Bug中有至少1/3的Bug是由序列化技术导致的,那这还得了啊,那这就很不得了了,也是因为它的技术简单用法便捷导致了大量的使用让很多程序员都受到了其影响。所以在官方发布了这个公告后就很伤心,蓝瘦香菇,那又一想就觉得有无所谓,反正还没影响到我,出点问题都是小问题,都能处理。这个时候就体现了官方的严谨性了,官方说不行,这个技术有问题,我就是要舍弃它,所以官方又说了,我们会在之后的几个版本逐渐的舍弃掉它。就没了,就之后再使用中,jdk一更新发现就没有这个类库了,这可能吗,不太可能,因为它的使用太广泛了,所以在Java官方没有推出一个可以取代Java序列化的技术之前都不太可能。那说这么多的意思是干什么呢?就是俩字:不推荐。因为它有可能会被舍弃,被删除,那你如果使用序列化技术很多,到时候删除了就会产生很多Bug无法修复,程序就会崩溃。那又有同学要问了?你在这写博客推广呢还是阻止不要我们学呢?那你还写这篇博客干什么呢?好家伙,你们这个三连我是没想到啊,三连不是(点赞,收藏,加关注)吗?好,正式回答一下上面的问题啊,那我把这段话放到意义中是有它的意义的,那就是我们可以不使用但是要知道这个技术,要明白它的原理,它到底是什么。说个烂俗点,大家天天了解跑车,好表,那啥人等等,是我们拥有吗?不是,我们就是了解,为了身心愉悦,为了关键时刻可以装逼。没错,身为一个应用十分广泛的技术,你不知道它合理吗?很合理,但是在学习完这篇博客后,你还不清楚,那也不合理,所以还是希望大家能学到知识。
举个栗子
那我们先写一个例子来观察运行一下,之后再进行系统的学习。
结果展示
public class Demo14 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化与反序列化
//序列化
Book b = new Book("金苹果","讲述了种植过程");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://book.txt"));
oos.writeObject(b);//写
oos.close();
//反序列化
/* ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://book.txt"));
//Object o = ois.readObject();//读
// System.out.println(o);
Book o = (Book)ois.readObject();
System.out.println(o.getInfo());
System.out.println(o.getName());*/
}
static class Book implements Serializable {//标记
//标记:就是你要实现的对象是Serializable接口的实现类的对象的话就允许写出
//Book类中的所有属性也要实现Serializable接口
private String name;
private String info;
private Person user;
public Book(String name, String info) {
this.name = name;
this.info = info;
}
public Book() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", info='" + info + '\'' +
'}';
}
}
}
运行结果:
很明显,我们看不懂,那其实是你传输的是字符流,我们要用字符流转成字节流才能输出正确内容。(个人理解)。
从上到下依次分析
好,我们从上到下依次对例子中的知识点进行分析。
那其实最开始的源码并不是这样的,而是这样的:
public class Demo14 {
public static void main(String[] args) throws IOException{
//序列化与反序列化
//序列化
Book b = new Book("金苹果","讲述了种植过程");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://book.txt"));
oos.writeObject(b);
oos.close();
//反序列化
/*ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://book.txt"));
//Object o = ois.readObject();
// System.out.println(o);
Book o = (Book)ois.readObject();
System.out.println(o.getInfo());
System.out.println(o.getName());*/
}
注意:变化,这次我们的Book类的变化
static class Book {//标记
//标记:就是你要实现的对象是Serializable接口的实现类的对象的话就允许写出
//Book类中的所有属性也要实现Serializable接口
private String name;
private String info;
private Person user;
错误指出,这是com.java.Demo.Demo14中的Book类不能序列化异常。那这里就牵扯到了一个知识点:
ObjectOutputStream将Java对象的原始数据类型和图形写入OutputStream。
只有支持java.io.Serializable接口的对象才能写入流。
API的链接:最新版API JDK11版本中文释义
那我们通过API找到ObjectOutputStream 可以看到。
好,我们往下看
那通过查看API就找到为什么会报出异常的原因了,因为我们的Book类没有实现我们的Serializable接口。
Serializable标记接口
那其实当我们首次添加实现Serializable接口的时候会发现,我们的程序并没有报错,也就是说这个Serializable接口并没有让我们重写它的抽象方法,那我们打开一看可以发现出现了这样的一幕:
这个接口没有任何抽象方法,这种接口就是所谓的标记接口,实现它就是为了标记我们的Book类是Serializable接口的子类,就是你要实现的对象是Serializable接口的实现类的对象的话就允许写出。
还要注意的是:一定要及时的去释放流,去释放内存空间
先记住这个接口之后进行更详细的讲解。
那我们重新回到上面的源码,那之后的反序列化打印就是:
由此,这个例子就这样圆满完后了。
实现序列化
好,接下来,我们就要开始讲解我们的Serializable接口和Externalizable接口,这两个接口实现序列化的完整过程。
我们再次重申:Java序列化是指把Java对象转换为字节序列的过程,Java反序列化是指把字节序列恢复为Java对象的过程。通过序列化和反序列化实现网络传输、本地存储的目的。
Serializable接口实现序列化
要实现Java对象的序列化,只要将类实现标识接口——Serializable接口即可,不需要我们重写任何方法就可以实现序列化。那我们可以在学习之前看看API中时如何描述此接口的。
OK,那我划红线的地方呢也是此接口或者设计此接口需要注意的点,之后我们进行源码的编写,来了解一下。
首先包分类如下:
之后就开始编写了。
编写实体类
package ocm.java.bean;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.List;
/**
* 学生实体类
*/
public class Student implements Serializable {
/**
* 学号
*/
private String stuNum;
/**
* 姓名
*/
private String stuName;
/**
* 教师姓名:一个学生可以有多个老师
*/
private List<String> teacherList;
//无参数构造方法
public Student() {
}
//全参构造方法
public Student(String stuNum, String stuName, List<String> teacherList) {
this.stuNum = stuNum;
this.stuName = stuName;
this.teacherList = teacherList;
}
@Override
public String toString() {
return "Student{" +
"stuNum='" + stuNum + '\'' + ", stuName='" + stuName + '\'' +
", teacherList=" + teacherList +
'}';
}
public String getStuNum() {
return stuNum;
}
public void setStuNum(String stuNum) {
this.stuNum = stuNum;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public List<String> getTeacherList() {
return teacherList;
}
public void setTeacherList(List<String> teacherList) {
this.teacherList = teacherList;
}
}
编写Java对象序列化工具类
package ocm.java.util;
import java.io.*;
/**
* 序列化和反序列化的工具类
*/
public class MySerializeUtil {
/**
* 将对象序列化到指定文件中
*
* @param obj
* @param fileName
*/
public static void mySerialize(Object obj, String fileName) throws IOException {
//输出流
OutputStream out = new FileOutputStream(fileName);
//对象序列化的流,表示对象输出流
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(obj);//序列化,对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中
objOut.close();
}
}
测试对象的序列化类
package ocm.java.test;
import ocm.java.bean.Student;
import ocm.java.util.MySerializeUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class MainTest {
public static void main(String[] args) {
List<String> teacherList = new ArrayList<>();
teacherList.add("空空道人");
teacherList.add("贾代儒");
Student stu1 = new Student("1001","贾宝玉",teacherList);
System.out.println("原始对象:"+ stu1);
//对象序列化
String fileName = "stu01.txt";
try {
MySerializeUtil.mySerialize(stu1,fileName);
System.out.println("序列化对象完毕!OK!");
} catch (IOException e) {
e.printStackTrace();
}
//对象的反序列化
}
}
那最终结果打印出来就是:
建议大家可以分开进行源码测试,首先测试序列化,之后再进行反序列化。
编写实体类的反序列化
那这一段源码其实就是完全没有变化,也不需要变。
package ocm.java.bean;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.List;
/**
* 学生实体类
*/
public class Student implements Serializable {
/**
* 学号
*/
private String stuNum;
/**
* 姓名
*/
private String stuName;
/**
* 教师姓名:一个学生可以有多个老师
*/
private List<String> teacherList;
//无参数构造方法
public Student() {
}
//全参构造方法
public Student(String stuNum, String stuName, List<String> teacherList) {
this.stuNum = stuNum;
this.stuName = stuName;
this.teacherList = teacherList;
}
@Override
public String toString() {
return "Student{" +
"stuNum='" + stuNum + '\'' + ", stuName='" + stuName + '\'' +
", teacherList=" + teacherList +
'}';
}
public String getStuNum() {
return stuNum;
}
public void setStuNum(String stuNum) {
this.stuNum = stuNum;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public List<String> getTeacherList() {
return teacherList;
}
public void setTeacherList(List<String> teacherList) {
this.teacherList = teacherList;
}
}
编写Java对象反序列化工具类
而我们在工具类中直接添加一个反序列化的方法即可。
/**
* 从指定文件中反序列化对象
*
* @param fileName
* @return 1.1.3 测试对象的序列化和反序列化
* 运行结果:
*/
public static Object myDeserialize( String fileName) throws IOException, ClassNotFoundException {
//输入流,
InputStream in = new FileInputStream(fileName);
//表示对象输入流
ObjectInputStream objIn = new ObjectInputStream(in);
Object obj = objIn.readObject();//读取字节序列,再把它们反序列化成为一个对象,并将其返回
return obj;
}
测试对象的反序列化类
志远,这一块是变化最大的,但其实也很简单,就是调用了之前反序列化的方法,然后做了一个判断,判断是不是Student类,然后将obj对象强转为Student类,抛出异常即可。
try {
//对象序列化
MySerializeUtil.mySerialize(stu1,fileName);
System.out.println("序列化对象完毕!OK!");
//stu1.setStuName("薛宝钗");//序列化完毕之后修改的属性
//对象的反序列化
Object obj = MySerializeUtil.myDeserialize(fileName);
if (obj instanceof Student){
Student stuNew = (Student) obj;
System.out.println("反序列化:"+stuNew);
}
} catch (Exception e) {
e.printStackTrace();
}
那再次运行结果也是给出了正确的答案:
这里对上面的2个操作文件流的类的简单说明,这也是之前就在API中提到的
ObjectOutputStream代表对象输出流:
它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流:
它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
部分属性的序列化
- 实现部分字段序列化的方式:
- 使用transient修饰符
- 使用static修饰符
- 默认方法writeObject和readObject。
- 稍后讲解的另一个接口。
1、使用transient修饰符
修改实体类,将实体类中不想序列化的属性添加transient修饰词。
重新运行测试类的结果:
我们将实体类中的stuName和teacherList属性添加了transient修饰词,因此对象被序列化的时候忽略
这两个属性。通过运行结果可以看出。
我们的stuName与teacherList属性并没有进行序列化。
2、使用static修饰符
static修饰符修饰的属性也不会参与序列化和反序列化。
修改实体类,将实体类中不想序列化的属性添加static修饰词。这次我们只对stuName进行修改,
然后修改测试类,在完成原始对象的序列化之后再对static修饰的变量进行一次赋值操作:
重新运行测试类的结果:
我们将实体类中的stuName属性添加了static修饰词,因此对象被序列化的时候忽略这个属性。通
过运行结果可以看出。
3、默认方法writeObject和readObject
修改实体类,将static修饰词去掉,添加两个方法。
private void writeObject(ObjectOutputStream objOut) throws IOException {
System.out.println("writeObject-----------");
objOut.writeObject(stuNum);
objOut.writeObject(stuName);
}
private void readObject(ObjectInputStream objIn) throws IOException, ClassNotFoundException {
System.out.println("readObject-----------");
stuNum = (String) objIn.readObject();
stuName = (String) objIn.readObject();
}
重新运行测试类的结果:
我们在添加的方法中只对stuNum和stuName属性做了序列化和反序列化的操作,因此只有这个两个属
性可以被序列化和反序列化。
3.1 源码分析:
注意:添加的两个方法必须是private void,否则不生效。
Java调用ObjectOutputStream类检查其是否有私有的、无返回值的writeObject方法,如果有,
其会委托该方法进行对象序列化。
Serializable接口的注释:
ObjectStreamClass类:在序列化(反序列化)的时候,ObjectOutputStream(ObjectInputStream)
会寻找目标类中的私有的writeObject(readObject)方法,赋值给变量
writeObjectMethod(readObjectMethod)。
通过上面这两段代码可以知道,如果writeObjectMethod != null(目标类中定义了私有的writeObject
方法),那么将调用目标类中的writeObject方法,如果如果writeObjectMethod == null,那么将调用
默认的defaultWriteFields方法来读取目标类中的属性。
readObject的调用逻辑和writeObject一样。
总结一下,如果目标类中没有定义私有的writeObject或readObject方法,那么序列化和反序列化的时
候将调用默认的方法来根据目标类中的属性来进行序列化和反序列化,而如果目标类中定义了私有的
writeObject或readObject方法,那么序列化和反序列化的时候将调用目标类指定的writeObject或
readObject方法来实现。
Externalizable实现Java序列化
刚刚我们说实现部分属性序列化的方式有多种,最后一种来啦!就是通过实现Eexternalizable接口。
Externalizable继承自Serializable,使用Externalizable接口需要实现readExternal方法和
writeExternal方法来实现序列化和反序列化。
来看看Externalizable接口的说明:
同样的,我们再来看看API中的介绍:
编写实体类
这次就不再一步一步去实现了,基本内容一致。其实也就是实体类需要重新编写,剩下的都是不需要的。
package ocm.java.bean;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.List;
public class Student implements Externalizable {
/**
* 学号
*/
private String stuNum;
/**
* 姓名
*/
private String stuName;
/**
* 教师姓名:一个学生可以有多个老师
*/
private List<String> teacherList;
//无参数构造方法
public Student() {
}
//全参构造方法
public Student(String stuNum, String stuName, List<String> teacherList) {
this.stuNum = stuNum;
this.stuName = stuName;
this.teacherList = teacherList;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(stuNum);
out.writeObject(stuName);
//out.writeObject(teacherList);
//实现部分属性序列化直接注释掉就好
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
stuNum = (String) in.readObject();
stuName = (String) in.readObject();
//teacherList= (List<String>) in.readObject();
}
@Override
public String toString() {
return "Student{" +
"stuNum='" + stuNum + '\'' +
", stuName='" + stuName + '\'' +
", teacherList=" + teacherList +
'}';
}
public String getStuNum() {
return stuNum;
}
public void setStuNum(String stuNum) {
this.stuNum = stuNum;
}
public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public List<String> getTeacherList() {
return teacherList;
}
public void setTeacherList(List<String> teacherList) {
this.teacherList = teacherList;
}
}
运行结果和上面的一样:
Externalizable接口继承了Serializable接口,所以实现Externalizable接口也能实现序列化和反序列
化。
Externalizable接口中定义了writeExternal和readExternal两个抽象方法,这两个方法其实对应
Serializable接口的writeObject和readObject方法。可以这样理解:Externalizable接口被设计出来的
目的就是为了抽象出writeObject和readObject这两个方法,但是目前这个接口使用的并不多。
Serializable VS Externalizable
区别 | Serializable | Externalizable |
---|---|---|
实现复杂度 | 实现简单,Java对其有内建支持 | 实现复杂,由开发人员自己完成 |
执行效率 | 所有对象由Java统一保存,性能较低 | 开发人员决定哪个对象保存,可能造成速度提升 |
保存信息 | 保存时占用空间大 | 部分存储,可能造成空间减少 |
使用频率 | 高 | 偏低 |