详解序列化和反序列化

序列化的最终目的就是为了对象可以跨平台存储及进行网络传输。
进行跨平台存储和网络传输的方式就是IO,IO支持的数据格式是字节数组。
并且我们还需要将字节数组还原回对象的原来模样,因此需要在对象转换成字节数组时就制定一种规则–序列化

1、概念

把Java对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。

2、序列化的用途:

核心作用就是对象状态的保存和重建–字节流中所保存的对象状态及描述信息

  • 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中 --(持久化对象)
    –在某些应用中,需要对某些对象进行序列化,离开内存空间,入住物理硬盘,以便长期保存。
    –Web服务器中的Session对象,当有10万用户并发访问,可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些Session先序列化到硬盘中,需要用的时候,再把保存在硬盘中的对象还原到内存中。
  • 在网络上传送对象的字节序列 --(网络传输对象)
    –当两个进程在进行远程通信时,进程之间可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上进行传输。
    –发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方需要把字节序列再恢复为Java对象。

3、序列化的实现-API

1、java.io.ObjectOutputStream 代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

对象序列化的步骤:
创建一个对象输出流,可以包装一个其他类型的目标输出流(如文件输出流)
通过对象输出流的writeObject()方法写对象

2、java.io.ObjectInputStream 代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

对象反序列化的步骤:
创建一个对象输入流,它可以包装一个其他类型的源输入流(如文件输入流)
通过对象输入流的readObject()方法读取对象

4、序列化的使用-接口

只有实现了Serializable 和 Externalizable 接口的类的对象才能被序列化。

Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。

1、Serializable接口

一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。

这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。

由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。

使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

2、Externalizable接口

它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。

因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。

对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。

3、二者对比

使用时,你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。如果要定义很多的特殊处理,就可以使用Externalizable。

5、常见序列化方式:

JDK(不支持跨语言)、JSON、XML、Hessian、Kryo(不支持跨语言)、Thrift、Protostuff、FST(不支持跨语言)

(1) JDK原生序列化

JDK 自带的序列化机制对使用者而言是非常简单的。

序列化具体的实现是由 ObjectOutputStream 完成的,而反序列化的具体实现是ObjectInputStream 完成的。
详解序列化和反序列化_第1张图片

序列化过程就是在读取对象数据的时候,不断加入一些特殊分隔符,这些特殊分隔符用于在反序列化过程中截断用。

  • 头部数据用来声明序列化协议、序列化版本,用于高低版本向后兼容
  • 对象数据主要包括类名、签名、属性名、属性类型及属性值,当然还有开头结尾等数据,除了属性值属于真正的对象值,其他都是为了反序列化用的元数据
  • 存在对象引用、继承的情况下,就是递归遍历“写对象”逻辑

(2)JSON

JSON 是典型的 Key-Value 方式,没有数据类型,是一种文本型序列化框架

缺点:

  1. JSON 进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销;
  2. JSON 没有类型,但像 Java 这种强类型语言,需要通过反射统一解决,所以性能不会太好。

(3)Hessian

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。有非常好的兼容性和稳定性

缺点:

  1. 官方版本对 Java 里面一些常见对象的类型不支持:
  2. Linked 系列,LinkedHashMap、LinkedHashSet等,但是可以通过扩展CollectionDeserializer 类修复;
  3. Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
  4. Byte/Short 反序列化的时候变成 Integer。

(4)Protobuf

Protobuf 是 Google 内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf 使用的时候需要 定义 IDL(Interface description language),然后使用不同语言的 IDL编译器,生成序列化工具类,它的优点是:

  1. 序列化后体积相比 JSON、Hessian 小很多;
  2. IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似XML 解析器;
  3. 序列化反序列化速度很快,不需要通过反射获取类型;
  4. 消息格式升级和兼容性不错,可以做到向后兼容。

缺点:

  1. 对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如 Hessian
  2. 不支持 null;
  3. ProtoStuff 不支持单纯的 Map、List 集合对象,需要包在对象里面。

建议使用类型:

首选是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、兼容性和安全性上,都满足了我们的要求。其中 Hessian 在使用上更加方便,在对象的兼容性上更好;Protobuf则更加高效,通用性上更有优势。

6、serialVersionUID

字面意思是序列化的版本号;凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量。

这个serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。

类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。

在完成序列化和反序列的测试后,修改对象类,添加一个新的属性,再执行反序列化操作就会抛出异常信息:

—文件流中的class和classpath中的class,也就是修改后的class与序列化的内容不兼容了,出于安全机制考虑,抛出错误,拒绝载入。

— 如果必须需要在序列化后添加一个字段或方法,需要自己去指定serialVersionUID,因为这个UID是唯一的

7、Java序列化中的几个问题:

0、序列化时,只对对象的状态进行保存,而不管对象的方法

1、static属性不能被序列化–代表类的状态

–序列化保存的是对象的状态,静态变量属于类的状态,因此序列化不保存静态变量

2、Transient关键字修饰的属性不会被序列化–代表对象的临时数据

3、序列化版本号的作用:

对象通常需要根据业务的需求变化要新增、修改或者删除一些属性,在我们做了一些修改后,就通过修改版本号告诉 反序列化的那一方对象有了修改你需要同步修改。

4、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化

5、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口

序列化以正向递归的形式进行的,如果父类实现了序列化那么其子类都将被序列化;子类实现了序列化而父类没有实现序列化,那么只有子类的属性会进行序列化,而父类的属性是不会进行序列化的。

6、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现

7、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;

注意:浅拷贝请使用Clone接口的原型模式。

8、Serializable 中的writeObject()方法与readObject()方法 和 Externalizable 中的writeExternal()和readExternal() 方法有什么异同?

readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。

需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。

虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。

9、并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:

安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的;

资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现;

简单实现:

java 实现序列化很简单,只需要实现Serializable 接口即可。


public class User implements Serializable{
     
 //年龄
 private int age;
 //名字
 private String name ;

 public int getAge() {
     
 return age;
    }
 public void setAge(int age) {
     
 this.age = age;
    }

 public String getName() {
     
 return name;
    }

 public void setName(String name) {
     
 this.name = name;
    }
}

// 把User对象设置值后写入文件

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);

User user = new User();
user.setAge(18);
user.setName("sandy");
oos.writeObject(user);

oos.flush();
oos.close();

// 再把从文件读取出来的转换为对象

FileInputStream fis = new FileInputStream("D:\\temp.txt");
ObjectInputStream oin = new ObjectInputStream(fis);
User user = (User) oin.readObject();
System.out.println("name="+user.getName());
// 输出结果为:name=sandy


// 以上把User对象进行二进制的数据存储后,并从文件中读取数据出来转成User对象就是一个序列化和反序列化的过程。

你可能感兴趣的:(Java,网络,java,编程语言,序列化,Serializable)