序言
序列化是一种对象持久化的手段,普遍应用在网络传输、保存本地文件、数据库场景中
基本概念
序列化
:把对象转换为字节序列的过程称为对象的序列化
反序列化
:把字节序列恢复为对象的过程称为对象的反序列化
为什么要序列化
Java平台允许我们在内存中创建可复用的Java对象,但只有当JVM(Java虚拟机)处于运行时,这些对象才可能存在,也就是这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存指定的对象(持久化对象),并在将来重新读取被保存的对象。Java对象序列化就实现了该功能。
网络通信时,无论是何种类型的数据,都会转成字节序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。Java对象序列化也实现了该功能。
所以序列化机制会把内存中的Java对象转换成与平台无关的二进制流
,从而永久地保存在磁盘上或是通过网络传输到另一个网络节点。
举个场景例子
...
什么情况下你需要用到
- 网络上传输各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都是以二进制序列的形式在网络上传送的,那么发送方就需要将这些数据序列化为字节流后传输,而接收方接到字节流后需要反序列化为相应的数据类型。
- 当然接收方也可以将接收到的字节流存储到磁盘中,等到以后想恢复的时候再恢复。
可以得出对象的序列化和反序列化主要有两种用途:
- 把对象的字节序列永久地保存到磁盘上。(持久化对象)
- 可以将Java对象以字节序列的方式在网络中传输。(网络传输对象)
serialVersionUID
情境:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。
问题:C 对象的全类路径假设为 com.inout.Test,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。
解决:虚拟机是否允许反序列化,不仅取决于类路径
和功能代码
是否一致,一个非常重要的一点是两个类的序列化ID
是否一致(就是 private static final long serialVersionUID = 1L)。
实现
- Serializable
public interface Serializable {
}
- Externalizable
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException,ClassNotFoundException;
}
Externalizable
这里不做描述
静态变量序列化
public class Main2 implements Serializable {
private static final long serialVersionUID = 1L;
public static int staticVar = 5;
public static void main(String[] args) {
try {
//初始时staticVar为5
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("static_serializable_demo.obj"));
out.writeObject(new Main2());
out.close();
//序列化后修改为10
Main2.staticVar = 10;
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"static_serializable_demo.obj"));
Main2 t = (Main2) oin.readObject();
oin.close();
//再读取,通过t.staticVar打印新的值
System.out.println(t.staticVar);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出的结果是: 10
之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态
,静态变量属于类的状态,因此 序列化并不保存静态变量。
对敏感字段加密
情境:服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
解决:在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作
public class Main3 implements Serializable {
private static final long serialVersionUID = 1L;
private String password = "pass";
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private void writeObject(ObjectOutputStream out) {
try {
ObjectOutputStream.PutField putFields = out.putFields();
System.out.println("原密码:" + password);
password = "encryption";//假装加密
putFields.put("password", password);
System.out.println("加密后的密码" + password);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
ObjectInputStream.GetField readFields = in.readFields();
Object object = readFields.get("password", "");
System.out.println("要解密的字符串:" + object.toString());
password = "pass";//假装解密,需要获得本地的密钥
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("password_serializable_demo.obj"));
out.writeObject(new Main3());
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"password_serializable_demo.obj"));
Main3 t = (Main3) oin.readObject();
System.out.println("解密后的字符串:" + t.getPassword());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出:
原密码:pass
加密后的密码encryption
要解密的字符串:encryption
解密后的字符串:pass
序列化存储规则1
public class Main4 implements Serializable {
public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("rule_serializable_demo.obj"));
Main4 test = new Main4();
//试图将对象两次写入文件
out.writeObject(test);
out.flush();
System.out.println(new File("rule_serializable_demo.obj").length());
out.writeObject(test);
out.close();
System.out.println(new File("rule_serializable_demo.obj").length());
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"rule_serializable_demo.obj"));
//从文件依次读出两个文件
Main4 t1 = (Main4) oin.readObject();
Main4 t2 = (Main4) oin.readObject();
oin.close();
//判断两个引用是否指向同一个对象
System.out.println(t1 == t2);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
以上代码中对同一对象两次写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,然后从文件中反序列化出两个对象,比较这两个对象是否为同一对象。一般的思维是,两次写入对象,文件大小会变为两倍的大小,反序列化时,由于从文件读取,生成了两个对象,判断相等时应该是输入 false 才对,
但是最后结果输出:
56
61
true
我们看到,第二次写入对象时文件只增加了 5 字节,并且两个对象是相等的,这是为什么呢?
再看下面的例子
序列化存储规则2
public class Main5 implements Serializable {
public int i;
public String mString;
public static String fileName = "main5_serializable_demo.obj";
public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(fileName));
Main5 test = new Main5();
test.i = 1;
test.mString = "liu";
out.writeObject(test);
out.flush();
test.i = 2;
test.mString = "zhang";
out.writeObject(test);
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(fileName));
Main5 t1 = (Main5) oin.readObject();
Main5 t2 = (Main5) oin.readObject();
System.out.println(t1.toString());
System.out.println(t2.toString());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "Main5{" +
"i=" + i +
", mString='" + mString + '\'' +
'}';
}
}
输出结果:
Main5{i=1, mString='liu'}
Main5{i=1, mString='liu'}
原因是:
Java序列化遵循以下算法:
- 所有序列化过的,包括磁盘中的的实例对象都有一个序列化编号
- 当试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,当对象在本次虚拟机中从未被序列化过,则系统将其序列化为字节序列并输出
- 如果某个对象在本次虚拟机中已经序列化过,则直接输出这个序列化编号
鉴于以上的算法可能会造成一个潜在的问题:当序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会输出字节序列,而第二次调用时仅仅输出一个序列化编号,即使我们改变了这个对象的一些属性,这些改变后的属性也不会序列化到磁盘上
总结
- 在Java中,只要一个类实现了java.io.Serializable、Externalizable接口,那么它就可以被序列化。
- 通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化
- 虚拟机是否允许反序列化,取决于
类路径
&&功能代码
&&两个类的序列化ID
是否一致(就是 private static final long serialVersionUID) - 要想将父类对象也序列化,就需要让父类也实现Serializable 接口。
- Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
- 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
- 对象的类名、属性都会被序列化;而方法、static属性(静态属性)、transient属性(即瞬态属性)都不会被序列化
- 要序列化的对象的引用属性也必须是可序列化的,否则该对象不可序列化,除非以transient关键字修饰该属性使其不用序列化。
参考
https://blog.csdn.net/ShuSheng0007/article/details/80629348
https://blog.csdn.net/zcl_love_wx/article/details/52126876
https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html