一个标示性接口,接口中没有定义任何的方法或字段,仅用于标示可序列化的语义。
序列化时只对对象的状态进行保存,而不管对象的方法! 序列化前和序列化后的对象的关系为深拷贝;- 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)
反序列化对象类型是序列化对象的父类或本身
经测试发现: 在反序列化时,仍旧用的是序列化时的对象类型
![7b3dc37f2152c3cf7da947cfd195b92ea3a.jpg](https://oscimg.oschina.net/oscnet/7b3dc37f2152c3cf7da947cfd195b92ea3a.jpg)
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@Setterclass A extends B implements Serializable { public A(String a) { super(a); } private static final long serialVersionUID = 1L; private String a, b, c;}@Getter@Setterclass 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@NoArgsConstructorclass 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@NoArgsConstructorclass 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();}@Dataclass 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@AllArgsConstructorclass 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 佐证序列和反序列化与此无关 */@AllArgsConstructorclass Person implements java.io.Serializable { /** * */ private static final long serialVersionUID = -5941751315700344441L; private int age;}@Getter@AllArgsConstructorclass Student implements java.io.Serializable { /** * */ private static final long serialVersionUID = 1L; private String name; private Person person;}@Getter@AllArgsConstructorclass 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(); } }}