一个标示性接口,接口中没有定义任何的方法或字段,仅用于标示可序列化的语义。
序列化时只对对象的状态进行保存,而不管对象的方法! 序列化前和序列化后的对象的关系为深拷贝;
- Serialization(序列化)是一种将对象以一连串的字节描述的过程:首先要创建OutputStream(eg. FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中,调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。
- Deserialization(反序列化)是一种将这些字节重建成一个对象的过程反序列的过程(即将一个序列还原成为一个对象):
将一个InputStream(eg. FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()。
序列化 ID
序列化使用一个 hash,该 hash 是根据给定源文件中几乎所有东西(类路径、 方法名称、字段名称、字段类型、访问修改方法等) 通过运行 JDK serialver
命令计算出的,反序列化时将该 hash 值与序列化流中的 hash 值相比较,serialVersionUID 相同才能够被序列化。如果serialVersionUID类中没有指定,JVM将重新计算出serialVersionUID; 如果类几乎所有东西都相同仍然能够反序列化,否则不能。
java.io.InvalidClassException: com.noob.Person; local class incompatible: stream classdesc serialVersionUID = 7763748706987261198, local class serialVersionUID = 1279018472691830503
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1829)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1986)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at com.noob.TestSerializable.read(TestSerializable.java:41)
at com.noob.TestSerializable.main(TestSerializable.java:18)
如果在反序列化前修改了类路径(或者说JVM没有加载到原有的类),将报错:
eg. 修改了Person的类路径 由 com.noob 改为 com.noob.a
java.lang.ClassNotFoundException: com.noob.Person
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:677)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1819)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1713)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1986)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at com.noob.a.TestSerializable.read(TestSerializable.java:41)
at com.noob.a.TestSerializable.main(TestSerializable.java:18)
反序列化对象类型是序列化对象的父类或本身
经测试发现: 在反序列化时,仍旧用的是序列化时的对象类型
package com.noob;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import lombok.Data;
import com.noob.JSONUtils;
public class TestSerializable {
public static void main(String[] args) {
try {
/* 深复制 */
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream readIn = new ObjectOutputStream(bo);
A testA = new A();
testA.setA("a");
testA.setB("b");
testA.setC("c");
readIn.writeObject(testA);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream writeOut = new ObjectInputStream(bi);
B b = B.class.cast(writeOut.readObject()); // 反序列化对象类型是序列化对象的父类或本身
System.out.println(JSONUtils.toFormatJsonString(b));
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Getter
@Setter
class A extends B implements Serializable {
public A(String a) {
super(a);
}
private static final long serialVersionUID = 1L;
private String a, b, c;
}
@Getter
@Setter
class B implements Serializable {
private String a, b, c;
public B(String a) {
this.a = a;
}
}
如果A、B 两个类没有父子关系,程序异常:
java.lang.ClassCastException: Cannot cast com.noob.A to com.noob.B
at java.lang.Class.cast(Class.java:3369)
at com.noob.TestSerializable.main(TestSerializable.java:27)
即使没有无参的构造方法,也是可以序列化及反序列化的!
父类的序列化与 Transient 关键字
- 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
- 如果父类没有实现Serializable接口,没有提供默认构造函数,那么子类的序列化会出错;
如果父类不实现序列化接口则需要有默认的无参的构造函数。 在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值。
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值。
静态变量、成员方法、声明transient的变量 都不能被序列化和反序列化!
案例分析
eg. 使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?
根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化,形成类图如图 2 所示。
上图中可以看出,attr1、attr2、attr3、attr5 都不会被序列化,放在父类中的好处在于当有另外一个 Child 类时,attr1、attr2、attr3 依然不会被序列化,不用重复抒写 transient,代码简洁。
序列化允许重构
即使代码有一定的变化,但是serialVersionUID相同,仍然可以被反序列化。当出现新字段时会被设为缺省值。
eg. 将原有的Person写入到文件中。再修改Person类:staS改为非 static,firstName 改为非 transient , 增加属性addField。(static与transient 是/非 可互相转换,经测试都不能被正确序列化和反序列化)
package com.noob;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.zhongan.fcp.pre.allin.common.utils.JSONUtils;
public class TestSerializable {
public static void main(String[] args) {
/* write(); */
read();
}
private static void write() {
/* try {
Person ted1 = new Person("firstName", "lastName", 39);
FileOutputStream fos = new FileOutputStream("tempdata.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(ted1);
oos.close();
System.out.println("---serialize obj end.----");
} catch (Exception ex) {
ex.printStackTrace();
}*/
}
private static void read() {
try {
FileInputStream fis = new FileInputStream("D:/tempdata.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Person ted2 = (Person) ois.readObject();
ois.close();
System.out.println(JSONUtils.toFormatJsonString(ted2));
// Clean up the file
new File("tempdata.ser").delete();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person implements java.io.Serializable {
private static final long serialVersionUID = -5941751315700344441L;
private/**static**/String staS = "xxx"; // static 改为非 static
private/**transient**/String firstName; // transient 改为非 transient
private String lastName;
private int age;
private String addField; // 增加属性
}
对象引用的序列化
一个类要能被序列化,该类中的所有引用对象也必须是可以被序列化的。否则整个序列化操作将会失败,并且会抛出一个NotSerializableException,除非将不可序列化的引用标记为transient。
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = -5941751315700344441L;
private static String staS = "xxx"; // static 改为非 static
private transient String firstName; // transient 改为非 transient
private String lastName;
private int age;
private final Attribute attribute = new Attribute();
}
@Data
class Attribute {
private String lock;
}
java.io.NotSerializableException: com.noob.Attribute
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.noob.TestSerializable.write(TestSerializable.java:26)
at com.noob.TestSerializable.main(TestSerializable.java:17)
java.io.WriteAbortedException: writing aborted; java.io.NotSerializableException: com.noob.Attribute
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1539)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:2231)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2155)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2013)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1535)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)
at com.noob.TestSerializable.read(TestSerializable.java:36)
at com.noob.TestSerializable.main(TestSerializable.java:18)
Caused by: java.io.NotSerializableException: com.noob.Attribute
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.noob.TestSerializable.write(TestSerializable.java:26)
at com.noob.TestSerializable.main(TestSerializable.java:17)
序列化对象的引用关系
如果使用序列化机制向文件中写入了多个对象,在反序列化时,需要按实际写入的顺序读取。
JAVA的序列化机制采用了一种特殊的算法来保证序列化对象的关系:
所有保存到磁盘中的对象都有一个序列化编号。当程序试图序列化一个对象时,会先检查该对象是否已经被序列化过,只有该对象(在本次虚拟机中)从未被序列化,系统才会将该对象转换成字节序列并输出。如果对象已经被序列化,程序将直接输出一个序列化编号,而不是重新序列化。
package com.noob;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
public class TestSerializable {
public static void main(String[] args) {
try {
Person person1 = new Person(1000);
FileOutputStream fos = new FileOutputStream("D:/tempdata.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(person1);
oos.flush(); //可选
System.out.println(String.format("第一次将person1写入后的长度: %s", new File("D:/tempdata.ser").length()));
person1.setAge(5555); //修改属性值
oos.writeObject(person1);
System.out.println(String.format("再次将person1写入后的长度: %s", new File("D:/tempdata.ser").length()));
Person person2 = new Person(1000);
oos.writeObject(person2);
oos.close();
System.out.println(String.format("初始化新person2写入后的长度:%s ", new File("D:/tempdata.ser").length()));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/tempdata.ser"));
System.out.println("---deserialization obj begin.----");
Person deserialization_person1 = (Person) ois.readObject();
System.out.println("deserialization_person1的age: " + deserialization_person1.getAge());
System.out.println("deserialization_person1的内存: " + deserialization_person1);
Person deserialization_person2 = (Person) ois.readObject();
System.out.println("deserialization_person2的age: " + deserialization_person2.getAge());
System.out.println("deserialization_person2的内存: " + deserialization_person2);
System.out.println(String.format("deserialization_person1与deserialization_person2是否一致:%s ",
deserialization_person1 == deserialization_person2));
Person deserialization_person3 = (Person) ois.readObject();
System.out.println("deserialization_person3的内存: " + deserialization_person3);
System.out.println(String.format("deserialization_person1与deserialization_person3是否一致:%s ",
deserialization_person1 == deserialization_person3));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
@Getter
@Setter
@AllArgsConstructor
class Person implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = -5941751315700344441L;
private int age;
}
测试结果发现:
- 即使修改了age的属性值,但是deserialization_person2反序列化时仍然是原初始化值。
- 第二次写入对象时文件只增加了 5 字节(存储新增引用和一些控制信息),并且反序列化时恢复引用关系, 两个对象是相等的。
案例分析
流只能被读取一次。
eg. 如果序列化一个对象,反序列化时多次readObject,报错
try {
Person person = new Person(1000);
FileOutputStream fos = new FileOutputStream("D:/tempdata.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(person);
oos.close();
FileInputStream fis = new FileInputStream("D:/tempdata.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
Person person2 = (Person) ois.readObject();
Person person3 = (Person) ois.readObject();
ois.close();
} catch (Exception ex) {
ex.printStackTrace();
}
eg. 有两个Teacher对象,它们的Student实例变量都引用了同一个Person对象,而且该Person对象还另外一个引用变量引用它。如下图所示:
这里有三个对象per、t1、t2,如果都被序列化,会存在这样一个问题,在序列化t1的时候,会隐式的序列化person对象。在序列化t2的时候,也会隐式的序列化person对象。在序列化per的时候,会显式的序列化person对象。所以在反序列化的时候,会得到三个person对象,这样就会造成t1、t2所引用的person对象不是同一个。显然,这并不符合图中所展示的关系,也违背了java序列化的初衷。
package com.noob;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import lombok.AllArgsConstructor;
import lombok.Getter;
public class TestSerializable {
public static void main(String[] args) {
write();
read();
}
private static void write() {
try {
Person person = new Person(1000);
Student student = new Student("孙悟空", person);
Teacher teacher1 = new Teacher("唐僧", student);
Teacher teacher2 = new Teacher("菩提老祖", student);
FileOutputStream fos = new FileOutputStream("D:/tempdata.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(person);
oos.flush(); // 可选
oos.writeObject(student);
oos.flush();
oos.writeObject(teacher1);
oos.flush();
oos.writeObject(teacher2);
oos.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static void read() {
try {
FileInputStream fis = new FileInputStream("D:/tempdata.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
System.out.println("---deserialization obj begin.----");
Person person = (Person) ois.readObject();
System.out.println("person的内存: " + person);
Student student = (Student) ois.readObject();
System.out.println("student中person内存: " + student.getPerson());
System.out.println(String.format("student中person与直接反序列化的person是否一致:%s", student.getPerson() == person));
System.out.println("-------------------------------------------");
System.out.println("student的内存: " + student);
Teacher teacher1 = (Teacher) ois.readObject();
System.out.println("teacher1中student内存: " + teacher1.getStudent());
System.out
.println(String.format("teacher1中student与直接反序列化的student是否一致:%s", teacher1.getStudent() == student));
Teacher teacher2 = (Teacher) ois.readObject();
System.out.println("teacher2中student内存: " + teacher2.getStudent());
System.out
.println(String.format("teacher2中student与直接反序列化的student是否一致:%s", teacher2.getStudent() == student));
ois.close();
// Clean up the file
new File("tempdata.ser").delete();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
/**
* 此处没有getter/setter 佐证序列和反序列化与此无关
*/
@AllArgsConstructor
class Person implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = -5941751315700344441L;
private int age;
}
@Getter
@AllArgsConstructor
class Student implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String name;
private Person person;
}
@Getter
@AllArgsConstructor
class Teacher implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String name;
private Student student;
}
敏感字段加密
在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。
package com.noob;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectInputStream.GetField;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream.PutField;
public class Test implements java.io.Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String password = "origin_password";
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private void writeObject(ObjectOutputStream out) {
try {
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 {
GetField readFields = in.readFields();
Object object = readFields.get("password", "");
System.out.println("要解密的字符串:" + object.toString());
password = "origin_password";//此处可以通过私钥进行解密
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:/result.obj"));
out.writeObject(new Test());
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream("D:/result.obj"));
Test t = (Test) oin.readObject();
System.out.println("解密后的字符串:" + t.getPassword());
oin.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}