给我5分钟,一次性给你讲明白Java中的序列化和反序列化

原创
08/31 13:32
阅读数 0
写过文章的,想必都懂, 一篇原创文章是非常的费时费力的,而且大多数时候,还会扑街……

但是我还是要坚持写下去,因为我知道自己的知识体系还非常的不完善,很多知识点我还不了解,很多时候我们总觉得不会的太多,或者很多知识学了就忘,又或者大部分的技术文章我们没耐心看!

其实原因很简单,你的知识网还有很多漏洞,很多知识之间还没有建立起关联,很多知识点你还不了解,就拿今天这篇文章,在我没有系统化学习完这个知识点之前,当我看到相关的技术文章,我可能也不会看,但是当我今天学完这个知识点之后!

以后再遇到此类的知识点文章,我一定会看,我得看看有什么是我当初没有学到的,我得看看有哪些知识点讲解比我的理解更加精辟的,我得看看有什么独到的见解没有……

正所谓,温故而知新,而很多人都是没有这个故……


序列化是一个经常见到但是又被很多人忽视的知识点,重要吗?重要,经常见吗?是的,那你会吗?不会~ 那好,今天搞定它!

waht

什么是序列化

首先,你要明白,序列化它是一个过程,什么过程呢?

把一个java对象转化成字节序列的过程

java对象我们都知道,什么是字节序列呢?

字节,也就是byte,1byte = 8bit,也就是一个字节等于8位,每一位都是用0或者1来表示,在内存中,数据就是以二进制的形式存储的

那序列呢?简单看来说就好比排队,一列一列的,至此,字节序列,是不是就是像字节在排队一样,而字节又是一个个的8bit,理解了吧!

所以,Java中的序列化就是把Java对象变成二进制内容的一个过程,也就是从内存中把数据存储下来,而数据在内存中都是二进制的形式!

记住了,Java序列化出来的东西就是一个二进制内容,就是对象在内存中的存储形式!

变量存储角度理解序列化

接下来我们再以变量在内存中的存储为例去理解什么是序列化!

举一个例子,比如我们写一个变量:

String name = "张三";

也就是刚开始,我们把name的值设置成为”张三“,在后面的程序当中,我们可以修改这个name,比如我们又把name的值修改成为”李四“!

我们知道,程序的运行,需要把数据加载进内存才可以,也就是说,无论是最开始的”张三“还是后来的”李四“都会被加载进内存运行,那内存都会为这些变量分配内存!

可是,一旦程序执行完毕,变量所分配或者说所占用的内存就会被回收, 程序也就结束了,不过在这个过程中,我们可以从内存中把这些变量存储下来,那这个过程就叫做序列化!

反序列化

当你理解了什么是序列化之后,那反序列化也就不是问题了,自然而然的就懂了,所谓的反序列化用稍微专业点的话说就是:

把字节序列还原成对象的过程

由此可见,无论序列化还是反序列化,都是对象和字节序列之间的互相转换!

序列化的多样性

以上我们得知,序列化是一个对象和字节序列互相转换的过程,那随之而来的一个问题就是,该怎么转换?

也就是我们该如何实现把对象序列化成字节序列,然后再把字节序列反序列化成对象,这其中必然存在一种规则,序列化和反序列化都必须按照这个规则来!

那这个规则就是序列化协议,那由此我们基本可以得出,可能存在不同的序列化协议,然后有不同的方式去实现序列化。

也就是不管你咋搞,最终实现的目的是对象和字节序列的互相转换即可,比如我们的Java就有其自己实现的一套序列化机制,可以把Java对象序列化成字节序列,还可以把自己序列再通过反序列化还原成原来的对象!

除了Java,像我们熟知的json也有其自己的序列化技术,加入你用Java的序列化技术把一个对象序列化成了字节序列,那你用json的反序列化技术是无法将其还原成原本的Java对象的!

how

Java序列化demo演示

比如我们定义一个简单的Person类,包含以下简单属性:

private String name;
private int age;

接着我们就可以将其序列化,具体操作如下:

FileOutputStream outputStream = new FileOutputStream("C:\\Users\\ithuangqing\\desktop\\person.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

Person person = new Person();
person.setName("张三");
person.setAge(25);
objectOutputStream.writeObject(person);

objectOutputStream.flush();
objectOutputStream.close();

以上就是我们在Java中的序列化过程,我们执行该程序 ,会在我们的桌面生成一个person.txt的文本文件,查看下内容,发现是乱码:

我们可以使用专业的十六进制编辑器查看,可以看到相关的十六进制内容以及二进制内容,这个就是Java对象序列化后的字节序列了。

接着我们看看如何将其反序列化,也就是将其还原成原来的Java对象!

//反序列化
Person person = null;
File file;
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\ithuangqing\\desktop\\person.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
person = (Person) objectInputStream.readObject();

objectInputStream.close();
fileInputStream.close();

System.out.println(person.getName());

可以得到相应的结果,我们执行上述反序列化程序,可以得出:

正确得到我们之前设置的name,说明反序列化成功,以上就是在Java中的序列化和反序列化操作!

查看序列化出来的内容

我们可以查看序列化出来的字节序,会发现以txt文本形式查看是一些乱码,乱七八糟的,我们也看不懂,我们可以以二进制内容查看!

这些二进制内容,就是该Peroson对象在内存中的存储形式!这也是Java把对象序列化出来的二进制内容!

序列化条件Serializable

一个Java对象要想实现序列化的功能,那就必须实现一个接口,如下:

我们可以查看该接口的定义:

可以发现,这就是一个空接口,在Java中这样的接口叫做**“标记接口”英文叫做Marker Interface**,一般来说,一个类如果实现了一个标记接口,起到的作用仅仅是给自己增加一个标签,比如上述这个,如果一个类实现了这个接口,那就会给自己身增加一个“序列化”的标签,说明这个类可以实现序列化相关功能!

为什么要实现Serializable接口

为什么要实现Serializable接口呢难道就是为了让其实现序列化?说起来好像是这么回事,但是还是有一些深入的内容需要我们了解的!

首先我们来看两个类:

1、ObjectOutputStream 

2、ObjectInputStream

两个类:经过之前的代码演示,我们清楚了,对于Java的序列化而言就是通过上述两个数据流类来完成的,也就是说他们包含了序列化和反序列化对象的方法,分别如下:

我们先拿ObjectOutputStream来说,这个类包含很多写方法来写各种的数据类型,举一个例子来看:可以看到其中包含一个writeInt方法可以将我们的基本数据类型写到外部文件中,同样的我们则可以使用ObjectInputStream 再将外部的数据读取进来:

查看打印输出:

所以对于 ObjectInputStream 和 ObjectOutputStream这种高层次的数据流来说,它可以完成这样的一些操作,就是可以通过写操作将数据类型转换为字节流,而读操作又可以将字节流转换成特定的数据类型!

那对于Java这种高级面向对象编程语言而言,对象则是Java中所有数据的一个类型载体,因此也要可以对对象进行相应的读写,说白了就是需要让Java虚拟机知道在我们进行IO操作的时候,如何进行对象和字节流之间的转换,而Serializable接口就起到了这样的作用!

所以对于ObjectInputStream 和 ObjectOutputStream来说最主要的作用是进行Java序列化的操作,主要是因为他们提供了如下的两个方法:

1、序列化一个对象

public final void writeObject(Object x) throws IOException

2、反序列化对象

public final Object readObject() throws IOException, ClassNotFoundException

.ser文件

我们可以看这里的一步操作:

也就是我们把Java对象序列化出来的字节序列内容存储到了一个txt文本文件里面,不过,就好比我们写的与i写纯文本文件的格式txt,我们写的Java文件格式是java,也就是后缀名是.java,所以这里序列化出来的文件也有一个标准的格式叫做.ser

那为什么叫做这个呢?因为Java中要把一个对象序列化需要实现Serializable,看这个单词的开头,明白了吧~

我们操作一下:

执行该序列化程序,会得到该文件:

我们可以打开看下:

以二进制内容查看:

和之前的txt的内容是一致的!说这个只是希望大家后续在见到.ser文件知道这是一个序列化文件!

再看序列化是什么

到了这里我们再来看看,什么是Java的序列化:

把Java对象在内存中的状态给存储下来的一个过程,会得到一个该Java对象的字节序列,可以说是一个二进制内容,本质上是一个byte[]数组!

为什么说是byte[]数组呢?1byte = 8bit,也就是8位一组,也就是一个字节,而二进制内存都是0和1这种8位8位的,看下:

看着这张图,可以理解为什么是byte[]数组了吧!

why

序列化可以用在哪些地方

经过之前的描述我们知道了,通过序列化,我们可以把Java对象转换成字节序列,也就是二进制内容,这个内容中就包含了对象的相关数据,以及对象的类型信息和存储在对象中的数据的类型!

也就是说,我们通过反序列化是可以还原这个Java对象的,而且这个过程是Java虚拟机来操作的,是直接由Java虚拟机来构建这个对象,所以这个对象中的构造函数是不会得到执行的,因为根本不会经过这一步,虚拟机直接把对象给整出来了!

那我们就知道了,反序列化是由虚拟机来搞定的,那么也就是实现了,在一个平台上序列化出来的对象,完全可以放到另一个平台上去反序列化该对象!

因此,这就造成了,序列化可以用于一些特定的数据传输:

1、你想把内存中的对象保存起来,比如保存到数据库中或者保存到一个文件中,以便长期保存

2、使用socket进行网络数据传输

3、RMI(即远程调用Remote Method Invocation)的使用,也就是要利用对象序列化去运行远程主机上的服务,以达到就像在本地机上运行对象时一样。

序列化有哪些好处

那序列化有什么好处呢?或者说为什么要用序列化呢?

首先,我们知道,数据其实是比较复杂的,比如对象啊,文件啊,等等都有各自不同的数据格式,我们怎么能把它们统一保存呢?这不,序列化就可以啊,经过序列化之后,别管你是什么,都保存在一块了,都是字节序列,这就方便数据之间的传输了,因为格式统一!

另外,序列化保存的是对象在内存中的保存状态,反序列化的时候是由Java虚拟机直接将该类还原在内存中,简洁快速而高效!

还有就是有的时候需要把Java对象从内存中保存下来,以便脱离内存,存储在磁盘上,达到长期保存的目的,那这个序列化就很合适了!

序列化注意事项

异常

这里主要有两个异常:

1、ClassNotFoundException:没有找到对应的Class

2、InvalidClassException:Class不匹配

首先说一下第一个异常,也就是ClassNotFoundException,这个其实是比较简单的,也就是类没找到,什么类没找到呢?我们代码复现一下:

执行该代码,就会发生问题:

这个错误其实已经说的很明显了,没有找到Person这个类了,那是因为我把之前的Person改成Person1了,所以反序列化就找不到对应的类,这就是无法实现反序列化的:

我们再把Person1改回Person试一下:

也就是反序列化的时候,你得有个对应的类,而且这个类的路径位置啥的也是不能改变的,你看:

接下来我们看下InvalidClassException这个也就是Class不匹配的问题,咋回事呢?

同样,我们通过代码来演示:

为什么会出现这个错误呢?你看这里:

之前是int,反序列化的时候你这成了long,可不就是不匹配嘛,所以这样也是会出错的!

所以啊,总的来说,就是,你序列化的时候是什么样子的,我反序列化就给你个一摸一样的,但是如果你就没有定义这样的一个类,那我反序列化肯定就出错了!

如何解决:serialVersionUID

为了解决InvalidClassException的问题,就出现了这么一个东西,我们先看代码:

这个操作大家应该不陌生吧,应该都见过的,它的出现主要就是为了解决Class类型不匹配的问题,它作为标识Java类的序列化版本,可以看作是一个标记,一般来说,可以由我们的IDE自动生成,如果我们修改了类中的字段什么的,那这个serialVersionUID就会发生改变,通过这样的一种机制来自动阻止不匹配的class版本!

我们在之前进行序列化操作的时候并没有创建这个serialVersionUID,但是,没有显示创建就并不代表它没有,而是会生成默认的serialVersionUID,但是,在实际当中,Java官方建议我们还是要显示的创建serialVersionUID!

为啥,因为如果你不显示创建,那么默认的生成时高度依赖于JVM的,但是如果序列化和反序列化是跨平台操作的化,就有可能会发生前后创建的serialVersionUID不一致从而导致反序列化出现异常,所以还是要显示创建serialVersionUID,保证即使跨平台这个serialVersionUID也是一致的!

那serialVersionUID是如何起作用的呢?

说的简单点就是,序列化的时候这个serialVersionUID也被序列化进去了,那在反序列化的时候,JVM就会把传进来的字节流中的serialVersionUID与本地对应的类中的serialVersionUID进行比较,看是否一致,一致就可以反序列化,不一致就不能反序列化,看代码演示:

我们这里显示创建serialVersionUID,然后我们进行序列化操作生成新的字节序列内容,在反序列化之前,我们把这个serialVersionUID给修改下:

然后我们进行反序列化操作:

产生异常了,看来是类不匹配啊,我们看下这个异常描述:

说的是不是很清楚了!

那这个时候,我们再次将起修改成1L,然后我们做如下操作:

那根据我们之前说的,这里修改了类,那反序列化的化就会出现InvalidClassException 问题,那我们执行反序列化看下:

果然报错,类型问题,不过我们再来看一个情况:

我们再反序列化试下:

正常输出,没有报错,但是如果你没有显示自定义serialVersionUID的话,那就会由系统自定义生成,那就会报错了!不信你自己试试!

所以一定要注意serialVersionUID的加与不加的一些区别和可能会产生的问题!也就是说序列化完成之后,如果原类型字段增加或者减少,不指定serialVersionUID的情况下,也是会报不一致的错误。指定了则不报错!

Java的序列化安全嘛

以上我们较为详细的介绍了Java的序列化操作,那么现在来思考这样的一个问题,Java的序列化安全嘛?也就是使用Java序列化的操作会不会产生什么安全隐患?

想象一下这个,就是,序列化生成的字节序列我们可以通过反序列化直接将其在内存中还原成原状态,那如果某个字节序列是特意设置好的,含有一些不安全代码,那直接给还原到内存中了,是不是会产生一些安全问题!

所以Java中提供的序列化机制,本身是存在一些安全性问题的,那更好的办法是啥呢?我们可以通过使用json来实现,这样输出的数据都是一些基本类型的内容,不像Java序列化那样,序列化输出的包含了很多对象相关信息!

由于篇幅原因,关于json与Java的序列化内容暂不介绍!

关于序列化的内容,到此结束,欢迎大家留言交流!



庆哥建立了Java学习交流群,感兴趣的可以添加庆哥好友,
我会经常分享学习资源在里面,大家共同进步!



LOVE

点个在看你最好看

本文分享自微信公众号 - 编码之外(ithuangqing)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部