JAVA反序列化基础 1.what is 序列化与反序列化
Java序列化 是指把Java对象转换为字节序列的过程;
Java反序列化 是指把字节序列恢复为Java对象的过程。
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类 型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
2.why need 从两方面讨论
内存中的对象是临时的,进程关闭就消失,为了实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上,可以把内存中的对象保存到一个文件中或者数据库中。需要的时候再把字节序列还原成原来的Java对象
3.几种常见的序列化和反序列化协议 XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点
SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于XML为序列化和反序列化协议的结构化消息传递协议,就是一套基于XML序列化协议的、严格规定了“如何在网络上传输结构化数据”的通信规则
JSON(Javascript Object Notation) Protobuf
4.序列化实现 只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列(不是则会抛出异常)
Serializable 接口
是 Java 提供的序列化接口,它是一个空接口
1 2 public interface Serializable {}
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。(ObjectOutputStream和ObjectInputStream是专门用于对象序列化/反序列化的核心类)
Serializable 接口的基本使用 通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
Serializable 接口的特点 1.序列化类的属性没有实现 Serializable 那么在序列化就会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import java.io.*;public class SerializableTest { public static void main (String[] args) { Student student = new Student ("小明" , 20 , new Color ("红色" )); try (ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("student_error.dat" ))) { oos.writeObject(student); System.out.println("序列化成功" ); } catch (IOException e) { e.printStackTrace(); } } } class Student implements Serializable { private static final long serialVersionUID = 1L ; private String name; private int age; private Color color; public Student (String name, int age, Color color) { this .name = name; this .age = age; this .color = color; } } class Color { private String colorName; public Color (String colorName) { this .colorName = colorName; } }
序列化Student对象时,JVM 递归检查到其color属性所属的Color类未实现Serializable接口,违反了Java序列化的规则。
2.在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数 来重新创建对象。
这里借用一个师傅的代码了
Animal 是父类,它没有实现 Serilizable 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Animal { private String color; public Animal () { System.out.println("调用 Animal 无参构造" ); } public Animal (String color) { this .color = color; System.out.println("调用 Animal 有 color 参数的构造" ); } @Override public String toString () { return "Animal{" + "color='" + color + '\'' + '}' ; } }
BlackCat 是 Animal 的子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class BlackCat extends Animal implements Serializable { private static final long serialVersionUID = 1L ; private String name; public BlackCat () { super (); System.out.println("调用黑猫的无参构造" ); } public BlackCat (String color, String name) { super (color); this .name = name; System.out.println("调用黑猫有 color 参数的构造" ); } @Override public String toString () { return "BlackCat{" + "name='" + name + '\'' +super .toString() +'\'' + '}' ; } }
SuperMain 测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class SuperMain { private static final String FILE_PATH = "./super.bin" ; public static void main (String[] args) throws Exception { serializeAnimal(); deserializeAnimal(); } private static void serializeAnimal () throws Exception { BlackCat black = new BlackCat ("black" , "我是黑猫" ); System.out.println("序列化前:" +black.toString()); System.out.println("=================开始序列化================" ); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream (FILE_PATH)); oos.writeObject(black); oos.flush(); oos.close(); } private static void deserializeAnimal () throws Exception { System.out.println("=================开始反序列化================" ); ObjectInputStream ois = new ObjectInputStream (new FileInputStream (FILE_PATH)); BlackCat black = (BlackCat) ois.readObject(); ois.close(); System.out.println(black); } }
输出结果
1 2 3 4 5 6 7 调用 Animal 有 color 参数的构造 调用黑猫有 color 参数的构造 序列化前:BlackCat{name='我是黑猫'Animal{color='black'}'} =================开始序列化================ =================开始反序列化================ 调用 Animal 无参构造 BlackCat{name='我是黑猫'Animal{color='null'}'}
从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。3. 一个实现 Serializable 接口的子类也是可以被序列化的。
4. 静态成员变量是不能被序列化
序列化是针对对象属性的,而静态成员变量是属于类的。
5. transient 标识的对象成员变量不参与序列化
在下面这个栗子中,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是写以下两个 private 方法:
1 2 private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOExceptionprivate void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException
从这两个方法的名字就可以看出分别是序列化写入数据和反序列化读取数据用的,那么这两个方法是在哪里使用呢?其实在序列化和反序列化过程中会通过反射调用的,具体下面会分析这个过程哦。
分析一下这个 transient 应用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 public class MyList implements Serializable { private String name; private transient Object[] arr; public MyList () { } public MyList (String name) { this .name = name; this .arr = new Object [100 ]; for (int i = 0 ; i < 30 ; i++) { this .arr[i] = i; } } @Override public String toString () { return "MyList{" + "name='" + name + '\'' + ", arr=" + Arrays.toString(arr) + '}' ; } private void writeObject (java.io.ObjectOutputStream s) throws java.io.IOException { s.defaultWriteObject(); for (int i = 0 ; i < 30 ; i++) { s.writeObject(arr[i]); } } private void readObject (java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); arr = new Object [30 ]; for (int i = 0 ; i < 30 ; i++) { arr[i] = s.readObject(); } } }
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class TransientMain { private static final String FILE_PATH = "./transient.bin" ; public static void main (String[] args) throws Exception { serializeMyList(); deserializeMyList(); } private static void serializeMyList () throws Exception { System.out.println("序列化..." ); MyList myList = new MyList ("ArrayList" ); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream (FILE_PATH)); oos.writeObject(myList); oos.flush(); oos.close(); } private static void deserializeMyList () throws Exception { System.out.println("反序列化..." ); ObjectInputStream ois = new ObjectInputStream (new FileInputStream (FILE_PATH)); MyList myList = (MyList) ois.readObject(); ois.close(); System.out.println(myList); } }
测试输出结果
6.Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片
serialVersionUID与兼容性问题
Serializable 与反射 / 内存碎片:实现 Serializable 接口的类,JVM 会通过反射机制遍历类的字段、方法来完成序列化(对象转字节)和反序列化(字节转对象)。反射会频繁创建临时对象,这些小对象回收后就会产生内存碎片,这也是为什么高并发场景会推荐用 Protostuff、Kryo 等替代原生序列化的原因。
序列化的核心目的就是把内存中的对象 “存起来”(比如写文件、传网络),之后再 “还原” 成对象,而 serialVersionUID (一个 long 类型的常量)就是这个 “存 或者取” 过程中的 “身份验证标识”。
5.序列化ID 还是上面提到的,在进行序列化时,需要有一个serialVersionUID字段,这便是序列化ID
1 private static final long serialVersionUID = 1L;
这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
即序列化ID是为了保证成功进行反序列化
如何生成这个 serialVersionUID呢?
使用 AS plugin 插件就可以生成
在JDK中,可以利用 JDK 的 bin 目录下的 serialver 工具产生这个serialVersionUID,对于 Student.class,执行命令:serialver com.example.seriable.Student