序列化与反序列化
序列化与反序列化的使用场景:
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
- 在网络上传送对象的字节序列。
面向对象的一大特性便是封装,我们将模型的属性和方法封装到一个类中,非静态属性在对象实例化时被赋予具体的值。
可以这样理解,对象是 JAVA 层面用于描述一个实体的数据结构,与 XML,JSON无异。在很多场景下,我们需要对其进行保存或传输,在相同或不同的 JVM 之间传递。
对象本身就是用于描述一个实体的数据的封装,序列化是将能够完整描述一个对象的特征数据提取出来;反序列化是通过可以完整描述一个对象的特征数据创建一个一摸一样的对象。面向对象的思维很重要,比如在理解 RPC 调用时为什么总是传递序列化的对象,其实就是在传递一种约定好的数据结构罢了。在对数据进行处理时,就 JAVA 来说以对象为载体比其它载体更加优雅,可读性更强。就像前端向后端传参我们总是将参数封装在 dto 中,不对参数进行封装直接使用 getParamter 也可以实现。但参数经过封装后,数据规范一目了然,代码更整洁,可维护性也更强。
这样也就可以理解,为什么 方法/静态属性 不会被序列化和反序列化,它们是被定义在类的结构体中的,而不是对象的结构体中,不是某一个对象特有的,而是某一类对象共有的。只要字节码文件被加载,方法/静态属性 就存在在 JVM 中,它们并不是某一个具体对象的特征,因此没有传递它们的必要。
我们看几个典型的使用场景:
1. Web 服务器中的 Session 会话对象,当有10万用户并发访问,就有可能出现10万个 Session 对象,显然这种情况内存可能是吃不消的。于是 Web 容器就会把一些 Session 先序列化,让他们离开内存空间,序列化到硬盘中,当需要调用时,再把保存在硬盘中的对象还原到内存中。
2. 当两个进程进行远程通信时,彼此可以发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。同样的序列化与反序列化则实现了 进程通信间的对象传送,发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
可以总结为两类用途:对对象进行持久化存储;在不同进程间传递对象。我们来看一下如何进行序列化和反序列化。最常使用的序列化方式有三种:
1. XML:把对象序列化成XML格式的文件,然后就可以通过网络传输这个对象或者把它储存进文件或数据库里了。我们也可以从中取出它并且反序列化成原来的对象状态。在JAVA中我们使用 JAXB库。
2. JSON:同样可以把对象序列化成JSON格式从而持久化保存对象。可以使用GSON库来实现。
3. 我们也可以使用 JDK 自身提供的序列化格式来持久化储存对象。比如在JAVA中可以通过实现序列化接口来序列化一个对象。下面以这种方式为例:
ObjectOutputStream out = null; try { out = new ObjectOutputStream( new FileOutputStream("C:\\Applications\\applications\\test") ); out.writeObject(family); System.out.println("序列化完成!"); } catch (IOException e) { e.printStackTrace(); } finally { closeOutputStream(out); }
对象的反序列化:
ObjectInputStream in = null; Family familyFromFile = null; try { in = new ObjectInputStream(new FileInputStream("C:\\Applications\\applications\\test")); familyFromFile = (Family) in.readObject(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e1) { e1.printStackTrace(); } finally { closeInputStream(in); }
需要被序列化的对象所属的类必须实现 Serializable 接口:
1、Serializable 接口的作用只是用来标识我们这个类是需要进行序列化,并且 Serializable 接口中并没有提供任何方法。
2、SerialVersionUid 序列化版本号的作用是用来区分我们所编写的类的版本,用于判断反序列化时类的版本是否一直,如果不一致会出现版本不一致异常。
3、transient 关键字,主要用来忽略我们不希望进行序列化的变量。
至于为什么要显示的声明 SerialVersionUid 原因有两点:
1. 当实现java.io.Serializable接口中没有显示的定义serialVersionUID变量的时候,JAVA序列化机制会根据Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。这样我们便不能通过 Uid 标识对象所属类的版本,如果它们的 Uid 一致但版本不一致(属性不同),虽然序列化可以正常的进行,但会造成不一致的属性无法被正常传递,这可能造成程序运行错误。
2. 不同版本的 JVM 生成默认 Uid 的算法可能不同,如果不手动指定,即使是同一个类的对象,也无法在不同版本的 JVM 进程间传递。因为 Uid 的不同会直接造成反序列化的失败。
我们还可以让需要被序列化的类实现 Externalizable 接口,它是Serializable接口的子类,有时我们不希望序列化那么多,可以使用这个接口,这个接口的writeExternal()和readExternal()方法可以指定序列化哪些属性;但是如果你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。
在使用序列化或反序列化的过程中需要注意以下几点:
1. 如果父类已经实现了Serializable序列化接口的话,其子类就不用再实现这个接口了,但是反过来就不成立了。
2. 只有非静态的数据成员才会通过序列化操作被序列化。
3. 静态(Static)数据成员和被标记了transient的数据成员在序列化的过程中将不会被序列化,因此,如果你不想要保存一个非静态的数据成员你就可以给标记上transient。
4. 当一个对象被反序列化时,这个对象的构造函数将不会在被调用的。
5. 需要实现序列化的对象如果内部引用了一个没有实现序列化接口的对象,在其序列化过程中将会发生错误,
看一个序列化与反序列化的完整例子:
public class Person implements Serializable { private static final long serialVersionUID = 7592930394427200495L; private String name; private int age; private String descreption; @Override public String toString() { return "[ name: " + name + ", age: " + age + ", descreption:" + descreption + " ]"; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getDescreption() { return descreption; } public void setDescreption(String descreption) { this.descreption = descreption; } }
public class Family implements Serializable { private static final long serialVersionUID = 759293039442892747L; private String name; private String location; private Person father; private Person mother; private Person son; @Override public String toString() { String rn = System.lineSeparator(); return "name:" + name + rn + "location:" + location + rn + "father:" + father.toString() + rn + "mother:" + mother.toString() + rn + "son:" + son.toString(); } public static long getSerialVersionUID() { return serialVersionUID; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } public Person getFather() { return father; } public void setFather(Person father) { this.father = father; } public Person getMother() { return mother; } public void setMother(Person mother) { this.mother = mother; } public Person getSon() { return son; } public void setSon(Person son) { this.son = son; } }
public static void main(String[] args) { Person father = new Person(); father.setAge(50); father.setName("father"); father.setDescreption("i am father"); Person mother = new Person(); mother.setAge(45); mother.setName("mother"); mother.setDescreption("i am mother"); Person son = new Person(); son.setAge(20); son.setName("son"); son.setDescreption("i am son"); Family family = new Family(); family.setFather(father); family.setMother(mother); family.setSon(son); family.setName("family"); family.setLocation("QingDao"); ObjectOutputStream out = null; try { out = new ObjectOutputStream( new FileOutputStream("C:\\Applications\\applications\\test") ); out.writeObject(family); System.out.println("序列化完成!"); } catch (IOException e) { e.printStackTrace(); } finally { closeOutputStream(out); } ObjectInputStream in = null; Family familyFromFile = null; try { in = new ObjectInputStream(new FileInputStream("C:\\Applications\\applications\\test")); familyFromFile = (Family) in.readObject(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e1) { e1.printStackTrace(); } finally { closeInputStream(in); } System.out.println("反序列化完成!"); if (familyFromFile == null) { return; } System.out.println(familyFromFile.toString()); } private static void closeInputStream(ObjectInputStream in) { if (in == null) { return; } try { in.close(); } catch (IOException e) { e.printStackTrace(); } } private static void closeOutputStream(ObjectOutputStream out) { if (out == null) { return; } try { out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } }
运行结果: