序列化
序列化
- 将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
下面是序列化和反序列化常见应用场景:
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
Java序列化操作
一个对象要想序列化,必须满足两个条件:
该类必须实现java.io.Serializable 接口,Serializable是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException。
该类的所有属性必须是可序列化的
不可被序列化的情况
- static修饰的字段:静态优先于非静态加载到内存中(静态优先于对象进入到内存中),被static修饰的成员变量不能被序列化
- transient修饰的字段:如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient关键字修饰。
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。
serialVersionUID 有什么作用?
序列化号 serialVersionUID
属于版本控制的作用。反序列化时,会检查 serialVersionUID
是否和当前类的 serialVersionUID
一致。如果 serialVersionUID
不一致则会抛出 InvalidClassException
异常。强烈推荐每个序列化类都手动指定其 serialVersionUID
,如果不手动指定,那么编译器会动态生成默认的 serialVersionUID
。
为什么不推荐使用 JDK 自带的序列化?
我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因:
- 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
- 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。
- 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。相关阅读:应用安全:JAVA 反序列化漏洞之殇 - Cryin、Java 反序列化安全漏洞怎么回事? - Monica。
1 |
|
ObjectOutputStream
ObjectOutputStream
用于将对象序列化为字节流。主要方法:
writeObject(Object obj)
:将对象写入输出流,实现对象的序列化。
示例:
1
2
3
4
5
6
7try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.dat"))) {
MyClass myObject = new MyClass(); // MyClass 需要实现 Serializable 接口
oos.writeObject(myObject);
} catch (IOException e) {
e.printStackTrace();
}
ObjectInputStream
ObjectInputStream
用于从字节流中反序列化对象。主要方法:
readObject()
:从输入流中读取对象,实现对象的反序列化。
示例:
1
2
3
4
5
6
7try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"))) {
MyClass myObject = (MyClass) ois.readObject();
// 对象已经被反序列化,可以使用 myObject 进行操作
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
注意:
对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException异常。
另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。
发生这个异常的原因如下:
- 该类的序列版本号与从流中读取的类描述符的版本号不匹配
- 该类包含未知数据类型
- 该类没有可访问的无参数构造方法
Properties属性集
java.util.Properties extends Hashtable<k,v> implements Map<k,v>
- Properties类表示了一个持久的属性集
- Properties 可保存在流中或从流中加载
- Properties集合是一个唯一和IO流相结合的集合
- 可以使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
- 可以使用Properties集合中的方法load,把硬盘中保存的文件(键值对)读取到集合中使用
- 属性列表中每个键及其对应值都是一个字符串,Properties集合是一个双列集合,key和value默认都是字符串
Properties集合有一些操作字符串的特有方法
Object setProperty(String key, String value):调用 Hashtable 的方法 put。
String getProperty(String key):通过key找到value值,此方法相当于Map集合中的get(key)方法
Set
1 |
|
store方法
可以使用Properties集合中的方法store,把集合中的临时数据,持久化写入到硬盘中存储
void store(OutputStream out, String comments)
void store(Writer writer, String comments)
OutputStream out:字节输出流,不能写入中文
Writer writer:字符输出流,可以写中文
String comments:注释,用来解释说明保存的文件是做什么用的,不能使用中文,会产生乱码,默认是Unicode编码
,一般使用””空字符串
1 |
|
load方法
可以使用Properties集合中的方法load,把硬盘中保存的文件(键值对),读取到集合中使用
void load(InputStream inStream)
void load(Reader reader)
InputStream inStream:字节输入流,不能读取含有中文的键值对
Reader reader:字符输入流,能读取含有中文的键值对
注意:
- 存储键值对的文件中,键与值默认的连接符号可以使用=,空格(其他符号)
- 存储键值对的文件中,可以使用#进行注释,被注释的键值对不会再被读取
- 存储键值对的文件中,键与值默认都是字符串,不用再加引号
1 |
|