1. 理论
Java 序列化是 JDK 1.1 时的特性:将 Java 对象转换为字节数组,便于存储或传输。此后,仍然可以将字节数组转换回 Java 对象原有的状态。
序列化的思想是“冻结”对象状态,传输对象状态(写到磁盘、通过网络传输等等);
反序列化的思想是“解冻”对象状态,重新获得可用的 Java 对象。
所有这些事情的发生要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用 Serializable 标识接口标记他们的类,从而 “参与” 这个过程。
再来看看序列化 Serializbale
接口的定义:
public interface Serializable {
}
明明就一个空的接口嘛,为什么能够保证实现了它的“类的对象”被序列化和反序列化?
在回答上述问题之前,我们先来创建一个类(只有两个字段,和对应的 getter/setter
),用于序列化和反序列化。
1 public class UserInfo {
2
3 private int UserId;
4
5
6 private String UserName;
7
8
9 public int getUserId() {
10 return UserId;
11 }
12
13 public void setUserId(int userId) {
14 UserId = userId;
15 }
16
17 public String getUserName() {
18 return UserName;
19 }
20
21 public void setUserName(String userName) {
22 UserName = userName;
23 }
24 }
再来创建一个测试类,通过 ObjectOutputStream
将对象信息写入到文件当中,实际上就是一种序列化的过程;再通过 ObjectInputStream
将对象信息从文件中读出来,实际上就是一种反序列化的过程。
1 import java.io.*;
2
3 public class Main {
4
5 public static void main(String[] args) {
6 WriteIntoFile();
7 System.out.println("Hello World!");
8 }
9 private static void WriteIntoFile(){
10
11 UserInfo userInfo = new UserInfo();
12 userInfo.setUserId(12);
13 userInfo.setUserName("Jerry");
14
15 // 把对象写到文件中
16 try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("12jerry"));){
17 objectOutputStream.writeObject(userInfo);
18 } catch (IOException e) {
19 e.printStackTrace();
20 }
21
22 // 从文件中读出对象
23 try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("12jerry")));){
24 UserInfo userInfo1 = (UserInfo) objectInputStream.readObject();
25 System.out.println(userInfo1);
26 } catch (IOException | ClassNotFoundException e) {
27 e.printStackTrace();
28 }
29 }
30 }
UserInfo没有实现 Serializbale
接口,所以在运行测试类的时候会抛出异常,堆栈信息如下:
"C:\Program Files\Java\jdk1.8.0_211\bin\java.exe" "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.1.3\lib\idea_rt.jar=34706:D:\Program Files\JetBrains\IntelliJ IDEA 2019.1.3\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_211\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_211\jre\lib\rt.jar;E:\IdeaProjects\TestSer\out\production\TestSer" Main
java.io.NotSerializableException: UserInfo
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.WriteIntoFile(Main.java:20)
at Main.main(Main.java:6)
java.io.WriteAbortedException: writing aborted; java.io.NotSerializableException: UserInfo
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1577)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at Main.WriteIntoFile(Main.java:27)
at Main.main(Main.java:6)
Caused by: java.io.NotSerializableException: UserInfo
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.WriteIntoFile(Main.java:20)
... 1 more
Hello World!
Process finished with exit code 0
顺着堆栈信息,我们来看一下 ObjectOutputStream
的 writeObject0()
方法。其部分源码如下:
1 if (obj instanceof String) {
2 writeString((String) obj, unshared);
3 } else if (cl.isArray()) {
4 writeArray(obj, desc, unshared);
5 } else if (obj instanceof Enum) {
6 writeEnum((Enum<?>) obj, desc, unshared);
7 } else if (obj instanceof Serializable) {
8 writeOrdinaryObject(obj, desc, unshared);
9 } else {
10 if (extendedDebugInfo) {
11 throw new NotSerializableException(
12 cl.getName() + "\n" + debugInfoStack.toString());
13 } else {
14 throw new NotSerializableException(cl.getName());
15 }
16 }
也就是说,ObjectOutputStream
在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable
,如果全都不是的话,抛出 NotSerializableException
。
假如 UserInfo 实现了 Serializable
接口,就可以序列化和反序列化了。
1 import java.io.Serializable;
2
3 public class UserInfo implements Serializable {
4 private int UserId;
5 private String UserName;
6 public int getUserId() {
7 return UserId;
8 }
9 public void setUserId(int userId) {
10 UserId = userId;
11 }
12 public String getUserName() {
13 return UserName;
14 }
15 public void setUserName(String userName) {
16 UserName = userName;
17 }
18
19 }
具体序列化的过程如下:
以 ObjectOutputStream
为例,它在序列化的时候会依次调用 writeObject()
→writeObject0()
→writeOrdinaryObject()
→writeSerialData()
→invokeWriteObject()
→defaultWriteFields()
。
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
desc.checkDefaultSerialize();
int primDataSize = desc.getPrimDataSize();
desc.getPrimFieldValues(obj, primVals);
bout.write(primVals, 0, primDataSize, false);
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
desc.getObjFieldValues(obj, objVals);
for (int i = 0; i < objVals.length; i++) {
try {
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
}
}
}
那反序列化呢?反序列化的过程如下:
以 ObjectInputStream
为例,它在反序列化的时候会依次调用 readObject()
→readObject0()
→readOrdinaryObject()
→readSerialData()
→defaultReadFields()
。
1 private void defaultWriteFields(Object obj, ObjectStreamClass desc)
2 throws IOException
3 {
4 Class<?> cl = desc.forClass();
5 desc.checkDefaultSerialize();
6
7 int primDataSize = desc.getPrimDataSize();
8 desc.getPrimFieldValues(obj, primVals);
9 bout.write(primVals, 0, primDataSize, false);
10
11 ObjectStreamField[] fields = desc.getFields(false);
12 Object[] objVals = new Object[desc.getNumObjFields()];
13 int numPrimFields = fields.length - objVals.length;
14 desc.getObjFieldValues(obj, objVals);
15 for (int i = 0; i < objVals.length; i++) {
16
17 try {
18 writeObject0(objVals[i],
19 fields[numPrimFields + i].isUnshared());
20 }
21 }
22 }
Serializable
接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。
1. static
和 transient
修饰的字段是不会被序列化的。
private static int
然后跑一边程序,发现输出的文件中是没有 Age的内容的;
为什么出现这种现象呢?
因为,序列化保存的是对象的状态,而 static 修饰的属性是属于类的状态,所以序列化是不会序列 static 修饰的属性;
除了 Serializable
之外,Java 还提供了一个序列化接口 Externalizable
。
有什么不同呢?
首先,把 UserInfo 类实现的接口 Serializable
替换为 Externalizable
。
可以看到语法提示需要实现两个方法:
1 package Model;
2
3 import java.io.*;
4
5 public class UserInfo implements Externalizable {
6
7 private int UserId;
8 private String UserName;
9 private static int Age=12;
10
11 public static int getAge() {
12 return Age;
13 }
14 public static void setAge(int age) {
15 Age = age;
16 }
17 public int getUserId() {
18 return UserId;
19 }
20 public void setUserId(int userId) {
21 UserId = userId;
22 }
23 public String getUserName() {
24 return UserName;
25 }
26 public void setUserName(String userName) {
27 UserName = userName;
28 }
29
30 public void writeExternal(ObjectOutput out) throws IOException {
31
32 }
33 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
34
35 }
36 }