Java 序列化

what

什么是序列化

首先要明白,序列化它是一个过程,什么过程呢?
把一个java对象转化成字节序列的过程
java对象都知道,那什么是字节序列呢?
字节,也就是byte,1byte = 8bit,也就是一个字节等于8位,每一位都是用0或者1来表示,在内存中,数据就是以二进制的形式存储的
那序列呢?简单看来说就好比排队,一列一列的,至此,字节序列,是不是就是像字节在排队一样,而字节又是一个个的8bit,理解了吧!
所以,Java中的序列化就是把Java对象变成二进制内容的一个过程,也就是从内存中把数据存储下来,而数据在内存中都是二进制的形式!
记住了,Java序列化出来的东西就是一个二进制内容,就是对象在内存中的存储形式!

变量存储角度理解序列化

接下来再以变量在内存中的存储为例去理解什么是序列化!
举一个例子,比如写一个变量:

  1. String name = "张三";

也就是刚开始,把name的值设置成为”张三“,在后面的程序当中,可以修改这个name,比如又把name的值修改成为”李四“!
程序的运行,需要把数据加载进内存才可以,也就是说,无论是最开始的”张三“还是后来的”李四“都会被加载进内存运行,那内存都会为这些变量分配内存!
可是,一旦程序执行完毕,变量所分配或者说所占用的内存就会被回收, 程序也就结束了,不过在这个过程中,可以从内存中把这些变量存储下来,那这个过程就叫做序列化!

反序列化

当理解了什么是序列化之后,那反序列化也就不是问题了,自然而然的就懂了,所谓的反序列化用稍微专业点的话说就是:
把字节序列还原成对象的过程
由此可见,无论序列化还是反序列化,都是对象和字节序列之间的互相转换!

序列化的多样性

以上得知,序列化是一个对象和字节序列互相转换的过程,那随之而来的一个问题就是,该怎么转换?
也就是该如何实现把对象序列化成字节序列,然后再把字节序列反序列化成对象,这其中必然存在一种规则,序列化和反序列化都必须按照这个规则来!
那这个规则就是序列化协议,那由此基本可以得出,可能存在不同的序列化协议,然后有不同的方式去实现序列化。
也就是不管怎么处理,最终实现的目的是对象和字节序列的互相转换即可,比如Java就有其自己实现的一套序列化机制,可以把Java对象序列化成字节序列,还可以把自己序列再通过反序列化还原成原来的对象!
除了Java,像熟知的json也有其自己的序列化技术,加入用Java的序列化技术把一个对象序列化成了字节序列,那用json的反序列化技术是无法将其还原成原本的Java对象的!

how

Java序列化demo演示

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

  1. private String name;
  2. private int age;

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

  1. FileOutputStream outputStream = new FileOutputStream("C:\\Users\\ithuangqing\\desktop\\person.txt");
  2. ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
  3. Person person = new Person();
  4. person.setName("张三");
  5. person.setAge(25);
  6. objectOutputStream.writeObject(person);
  7. objectOutputStream.flush();
  8. objectOutputStream.close();

以上就是在Java中的序列化过程,执行该程序 ,会在桌面生成一个person.txt的文本文件,查看下内容,发现是乱码:
2021-08-30-22-35-52-466443.png
可以使用专业的十六进制编辑器查看,可以看到相关的十六进制内容以及二进制内容,这个就是Java对象序列化后的字节序列了。
接着看看如何将其反序列化,也就是将其还原成原来的Java对象!

  1. //反序列化
  2. Person person = null;
  3. File file;
  4. FileInputStream fileInputStream = new FileInputStream("C:\\Users\\ithuangqing\\desktop\\person.txt");
  5. ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
  6. person = (Person) objectInputStream.readObject();
  7. objectInputStream.close();
  8. fileInputStream.close();
  9. System.out.println(person.getName());

可以得到相应的结果,执行上述反序列化程序,可以得出:
2021-08-30-22-35-52-584433.png
正确得到之前设置的name,说明反序列化成功,以上就是在Java中的序列化和反序列化操作!

查看序列化出来的内容

可以查看序列化出来的字节序,会发现以txt文本形式查看是一些乱码,乱七八糟的,也看不懂,可以以二进制内容查看!
2021-08-30-22-35-52-733432.png
这些二进制内容,就是该Peroson对象在内存中的存储形式!这也是Java把对象序列化出来的二进制内容!

序列化条件Serializable

一个Java对象要想实现序列化的功能,那就必须实现一个接口,如下:
2021-08-30-22-35-52-924432.png
可以查看该接口的定义:
2021-08-30-22-35-53-057431.png
可以发现,这就是一个空接口,在Java中这样的接口叫做“标记接口”英文叫做Marker Interface,一般来说,一个类如果实现了一个标记接口,起到的作用仅仅是给自己增加一个标签,比如上述这个,如果一个类实现了这个接口,那就会给自己身增加一个“序列化”的标签,说明这个类可以实现序列化相关功能!

为什么要实现Serializable接口

那为什么要实现Serializable接口呢难道就是为了让其实现序列化?说起来好像是这么回事,但是还是有一些深入的内容需要了解的!
首先来看两个类:
1、ObjectOutputStream
2、ObjectInputStream
两个类:经过之前的代码演示清楚了,对于Java的序列化而言就是通过上述两个数据流类来完成的,也就是说他们包含了序列化和反序列化对象的方法,分别如下:
2021-08-30-22-35-53-254434.png2021-08-30-22-35-53-457429.png
先拿ObjectOutputStream来说,这个类包含很多写方法来写各种的数据类型,举一个例子来看:2021-08-30-22-35-53-596434.png
可以看到其中包含一个writeInt方法可以将基本数据类型写到外部文件中,同样的则可以使用ObjectInputStream 再将外部的数据读取进来:
2021-08-30-22-35-53-782439.png
查看打印输出:
2021-08-30-22-35-53-894433.png
所以对于 ObjectInputStreamObjectOutputStream这种高层次的数据流来说,它可以完成这样的一些操作,就是可以通过写操作将数据类型转换为字节流,而读操作又可以将字节流转换成特定的数据类型!
那对于Java这种高级面向对象编程语言而言,对象则是Java中所有数据的一个类型载体,因此也要可以对对象进行相应的读写,说白了就是需要让Java虚拟机知道在进行IO操作的时候,如何进行对象和字节流之间的转换,而Serializable接口就起到了这样的作用!
所以对于ObjectInputStreamObjectOutputStream来说最主要的作用是进行Java序列化的操作,主要是因为他们提供了如下的两个方法:
1、序列化一个对象

  1. public final void writeObject(Object x) throws IOException

2、反序列化对象

  1. public final Object readObject() throws IOException, ClassNotFoundException

.ser文件

可以看这里的一步操作:
2021-08-30-22-35-54-150430.png
也就是把Java对象序列化出来的字节序列内容存储到了一个txt文本文件里面,不过,就好比写的与i写纯文本文件的格式txt,写的Java文件格式是java,也就是后缀名是.java,所以这里序列化出来的文件也有一个标准的格式叫做.ser
那为什么叫做这个呢?因为Java中要把一个对象序列化需要实现Serializable,看这个单词的开头,操作一下:
2021-08-30-22-35-54-380437.png
执行该序列化程序,会得到该文件:
2021-08-30-22-35-54-525432.png
可以打开看下:
2021-08-30-22-35-54-739434.png
以二进制内容查看:
2021-08-30-22-35-54-920434.png
和之前的txt的内容是一致的!说这个只是希望大家后续在见到.ser文件知道这是一个序列化文件!

再看序列化是什么

到了这里再来看看,什么是Java的序列化:
把Java对象在内存中的状态给存储下来的一个过程,会得到一个该Java对象的字节序列,可以说是一个二进制内容,本质上是一个byte[]数组!
为什么说是byte[]数组呢?1byte = 8bit,也就是8位一组,也就是一个字节,而二进制内存都是0和1这种8位8位的,看下:
2021-08-30-22-35-55-081431.png
看着这张图,可以理解为什么是byte[]数组了吧!

why

序列化可以用在哪些地方

经过之前的描述知道了,通过序列化,可以把Java对象转换成字节序列,也就是二进制内容,这个内容中就包含了对象的相关数据,以及对象的类型信息和存储在对象中的数据的类型!
也就是说,通过反序列化是可以还原这个Java对象的,而且这个过程是Java虚拟机来操作的,是直接由Java虚拟机来构建这个对象,所以这个对象中的构造函数是不会得到执行的,因为根本不会经过这一步,虚拟机直接把对象给整出来了!
那就知道了,反序列化是由虚拟机来搞定的,那么也就是实现了,在一个平台上序列化出来的对象,完全可以放到另一个平台上去反序列化该对象!
因此,这就造成了,序列化可以用于一些特定的数据传输:
1、把内存中的对象保存起来,比如保存到数据库中或者保存到一个文件中,以便长期保存
2、使用socket进行网络数据传输
3、RMI(即远程调用Remote Method Invocation)的使用,也就是要利用对象序列化去运行远程主机上的服务,以达到就像在本地机上运行对象时一样。

序列化有哪些好处

那序列化有什么好处呢?或者说为什么要用序列化呢?
首先,数据其实是比较复杂的,比如对象啊,文件啊,等等都有各自不同的数据格式,怎么能把它们统一保存呢?这不,序列化就可以啊,经过序列化之后,别管是什么,都保存在一块了,都是字节序列,这就方便数据之间的传输了,因为格式统一!
另外,序列化保存的是对象在内存中的保存状态,反序列化的时候是由Java虚拟机直接将该类还原在内存中,简洁快速而高效!
还有就是有的时候需要把Java对象从内存中保存下来,以便脱离内存,存储在磁盘上,达到长期保存的目的,那这个序列化就很合适了!

序列化注意事项

异常

这里主要有两个异常:
1、ClassNotFoundException:没有找到对应的Class
2、InvalidClassException:Class不匹配
首先说一下第一个异常,也就是ClassNotFoundException,这个其实是比较简单的,也就是类没找到,什么类没找到呢?代码复现一下:
2021-08-30-22-35-55-328432.png
执行该代码,就会发生问题:
2021-08-30-22-35-55-553430.png
这个错误其实已经说的很明显了,没有找到Person这个类了,那是因为把之前的Person改成Person1了,所以反序列化就找不到对应的类,这就是无法实现反序列化的:
2021-08-30-22-35-55-718433.png
再把Person1改回Person试一下:
2021-08-30-22-35-55-932435.png
也就是反序列化的时候,得有个对应的类,而且这个类的路径位置啥的也是不能改变的:
2021-08-30-22-35-56-149431.png
接下来看下InvalidClassException这个也就是Class不匹配的问题。
同样,通过代码来演示:
2021-08-30-22-35-56-345435.png
为什么会出现这个错误呢?看这里:
2021-08-30-22-35-56-447434.png
之前是int,反序列化的时候这成了long,不就是不匹配嘛,所以这样也是会出错的!
所以啊,总的来说,就是,序列化的时候是什么样子的,反序列化个一模一样的,但是如果就没有定义这样的一个类,那反序列化肯定就出错了!

如何解决:serialVersionUID

为了解决InvalidClassException的问题,就出现了这么一个东西,先看代码:
2021-08-30-22-35-56-820475.png
这个操作大家应该不陌生吧,应该都见过的,它的出现主要就是为了解决Class类型不匹配的问题,它作为标识Java类的序列化版本,可以看作是一个标记,一般来说,可以由IDE自动生成,如果修改了类中的字段什么的,那这个serialVersionUID就会发生改变,通过这样的一种机制来自动阻止不匹配的class版本!
在之前进行序列化操作的时候并没有创建这个serialVersionUID,但是,没有显式创建就并不代表它没有,而是会生成默认的serialVersionUID,但是,在实际当中,Java官方建议还是要显式的创建serialVersionUID
为啥,因为如果不显式创建,那么默认的生成时高度依赖于JVM的,但是如果序列化和反序列化是跨平台操作的化,就有可能会发生前后创建的serialVersionUID不一致从而导致反序列化出现异常,所以还是要显式创建serialVersionUID,保证即使跨平台这个serialVersionUID也是一致的!
serialVersionUID是如何起作用的呢?
说的简单点就是,序列化的时候这个serialVersionUID也被序列化进去了,那在反序列化的时候,JVM就会把传进来的字节流中的serialVersionUID与本地对应的类中的serialVersionUID进行比较,看是否一致,一致就可以反序列化,不一致就不能反序列化,看代码演示:
2021-08-30-22-35-56-965474.png
这里显式创建serialVersionUID,然后进行序列化操作生成新的字节序列内容,在反序列化之前,把这个serialVersionUID给修改下:
2021-08-30-22-35-57-068432.png
然后进行反序列化操作:
2021-08-30-22-35-57-263438.png
产生异常了,看来是类不匹配,看下这个异常描述:
2021-08-30-22-35-57-484433.png
说的是不是很清楚了!
那这个时候,再次将起修改成1L,然后做如下操作:
2021-08-30-22-35-57-670434.png
那根据之前说的,这里修改了类,那反序列化的化就会出现InvalidClassException 问题,那执行反序列化看下:
2021-08-30-22-35-57-842473.png
果然报错,类型问题,不过再来看一个情况:
2021-08-30-22-35-58-072432.png
再反序列化试下:
2021-08-30-22-35-58-165436.png
正常输出,没有报错,但是如果没有显式自定义serialVersionUID的话,那就会由系统自定义生成,那就会报错了!
所以一定要注意serialVersionUID的加与不加的一些区别和可能会产生的问题!也就是说序列化完成之后,如果原类型字段增加或者减少,不指定serialVersionUID的情况下,也是会报不一致的错误。指定了则不报错!

Java的序列化安全嘛

以上较为详细的介绍了Java的序列化操作,那么现在来思考这样的一个问题,Java的序列化安全嘛?也就是使用Java序列化的操作会不会产生什么安全隐患?
想象一下这个,就是,序列化生成的字节序列可以通过反序列化直接将其在内存中还原成原状态,那如果某个字节序列是特意设置好的,含有一些不安全代码,那直接给还原到内存中了,是不是会产生一些安全问题!
所以Java中提供的序列化机制,本身是存在一些安全性问题的,那更好的办法是啥呢?可以通过使用json来实现,这样输出的数据都是一些基本类型的内容,不像Java序列化那样,序列化输出的包含了很多对象相关信息!