序列化 (Serialization)将对象的状态信息转换为可以存储或传输的形式的过程。

一、序列化和反序列化的概念

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

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

对象的序列化主要有两种用途

1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;

2) 在网络上传送对象的字节序列。

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。

当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

二、JDK类库中的序列化API

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
  java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
  只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。
  对象序列化包括如下步骤:
  1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
  2) 通过对象输出流的writeObject()方法写对象。

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

serialVersionUID适用于Java的序列化机制。简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

具体的序列化过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

serialVersionUID有两种显示的生成方式:

一、是默认的1L,比如:private static final long serialVersionUID = 1L;

二、是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:

private static final long serialVersionUID = xxxxL;

当一个类实现了Serializable接口,如果没有显示的定义serialVersionUID,Eclipse会提供相应的提醒。面对这种情况,我们只需要在Eclipse中点击类中warning图标一下,Eclipse就会 自动给定两种生成的方式。如果不想定义,在Eclipse的设置中也可以把它关掉的,设置如下:
Window ==> Preferences ==> Java ==> Compiler ==> Error/Warnings ==> Potential programming problems
将Serializable class without serialVersionUID的warning改成ignore即可。

当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。

如果我们不希望通过编译来强制划分软件版本,即实现序列化接口的实体能够兼容先前版本,就需要显式地定义一个名为serialVersionUID,类型为long的变量,不修改这个变量值的序列化实体都可以相互进行串行化和反串行化。

下面用代码说明一下serialVersionUID在应用中常见的几种情况。

(1)序列化实体类

package com.sf.code.serial;import java.io.Serializable;public class Person implements Serializable {private static final long serialVersionUID = 123456789L;public int id;public String name;public Person(int id, String name) {this.id = id;this.name = name;}public String toString() {return "Person: " + id + " " + name;}
}

(2)序列化功能:

package com.sf.code.serial;import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;public class SerialTest {public static void main(String[] args) throws IOException {Person person = new Person(1234, "wang");System.out.println("Person Serial" + person);FileOutputStream fos = new FileOutputStream("Person.txt");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(person);oos.flush();oos.close();}
}

(3)反序列化功能:

package com.sf.code.serial;import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;public class DeserialTest {public static void main(String[] args) throws IOException, ClassNotFoundException {Person person;FileInputStream fis = new FileInputStream("Person.txt");ObjectInputStream ois = new ObjectInputStream(fis);person = (Person) ois.readObject();ois.close();System.out.println("Person Deserial" + person);}}

情况一:假设Person类序列化之后,从A端传输到B端,然后在B端进行反序列化。在序列化Person和反序列化Person的时候,A端和B端都需要存在一个相同的类。如果两处的serialVersionUID不一致,会产生什么错误呢?
【答案】可以利用上面的代码做个试验来验证:
先执行测试类SerialTest,生成序列化文件,代表A端序列化后的文件,然后修改serialVersion值,再执行测试类DeserialTest,代表B端使用不同serialVersion的类去反序列化,结果报错:

Exception in thread "main" java.io.InvalidClassException: com.sf.code.serial.Person; local class incompatible: stream classdesc serialVersionUID = 1234567890, local class serialVersionUID = 123456789at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621)at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)at com.sf.code.serial.DeserialTest.main(DeserialTest.java:13)

情况二:假设两处serialVersionUID一致,如果A端增加一个字段,然后序列化,而B端不变,然后反序列化,会是什么情况呢?

package com.sf.code.serial;import java.io.Serializable;public class Person implements Serializable {private static final long serialVersionUID = 1234567890L;public int id;public String name;public int age;public Person(int id, String name) {this.id = id;this.name = name;}public Person(int id, String name, int age) {this.id = id;this.name = name;this.age = age;}public String toString() {return "Person: " + id + ",name:" + name + ",age:" + age;}
}
public class SerialTest {public static void main(String[] args) throws IOException {Person person = new Person(1234, "wang", 100);System.out.println("Person Serial" + person);FileOutputStream fos = new FileOutputStream("Person.txt");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(person);oos.flush();oos.close();}
}

Person DeserialPerson: 1234,name:wang

【答案】新增 public int age; 执行SerialTest,生成序列化文件,代表A端。删除 public int age,反序列化,代表B端,最后的结果为:执行序列化,反序列化正常,但是A端增加的字段丢失(被B端忽略)。

情况三:假设两处serialVersionUID一致,如果B端减少一个字段,A端不变,会是什么情况呢?

package com.sf.code.serial;import java.io.Serializable;public class Person implements Serializable {private static final long serialVersionUID = 1234567890L;public int id;//public String name;public int age;public Person(int id, String name) {this.id = id;//this.name = name;}public String toString() {return "Person: " + id //+ ",name:" + name + ",age:" + age;}
}

Person DeserialPerson: 1234,age:0

【答案】序列化,反序列化正常,B端字段少于A端,A端多的字段值丢失(被B端忽略)。

情况四:假设两处serialVersionUID一致,如果B端增加一个字段,A端不变,会是什么情况呢?
验证过程如下:
先执行SerialTest,然后在实体类Person增加一个字段age,如下所示,再执行测试类DeserialTest.

package com.sf.code.serial;import java.io.Serializable;public class Person implements Serializable {private static final long serialVersionUID = 1234567890L;public int id;public String name;public int age;public Person(int id, String name) {this.id = id;this.name = name;}/*public Person(int id, String name, int age) {this.id = id;this.name = name;this.age = age;}*/public String toString() {return "Person: " + id + ",name:" + name + ",age:" + age;}
}

结果:Person DeserialPerson: 1234,name:wang,age:0

说明序列化,反序列化正常,B端新增加的int字段被赋予了默认值0。

最后通过下面的图片,总结一下上面的几种情况。

静态变量序列化
情境:查看清单 2 的代码。

清单 2. 静态变量序列化问题代码

public class Test implements Serializable {private static final long serialVersionUID = 1L;public static int staticVar = 5;public static void main(String[] args) {try {//初始时staticVar为5ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));out.writeObject(new Test());out.close();//序列化后修改为10Test.staticVar = 10;ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));Test t = (Test) oin.readObject();oin.close();//再读取,通过t.staticVar打印新的值System.out.println(t.staticVar);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}}
}

清单 2 中的 main 方法,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来。依照清单 2,这个 System.out.println(t.staticVar) 语句输出的是 10 还是 5 呢?

最后的输出是 10,对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。

父类的序列化与 Transient 关键字

情境:一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。

解决:要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

特性使用案例

我们熟悉使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化,形成类图如图 2 所示。

图 2. 案例程序类图

上图中可以看出,attr1、attr2、attr3、attr5 都不会被序列化,放在父类中的好处在于当有另外一个 Child 类时,attr1、attr2、attr3 依然不会被序列化,不用重复抒写 transient,代码简洁。

static final 修饰的serialVersionUID如何被写入到序列化文件中的,看下面的源码:
序列化写入时的ObjectStreamClass.java中,

void writeNonProxy(ObjectOutputStream out) throws IOException {out.writeUTF(name);out.writeLong(getSerialVersionUID());byte flags = 0;...
public long getSerialVersionUID() {// REMIND: synchronize instead of relying on volatile?if (suid == null) {suid = AccessController.doPrivileged(new PrivilegedAction<Long>() {public Long run() {return computeDefaultSUID(cl);}});}return suid.longValue();}

一篇搞懂java序列化Serializable相关推荐

  1. 你了解java序列化吗?一篇文章搞懂java序列化

    程序员的成长之路 互联网/程序员/技术/资料共享 关注 阅读本文大概需要 9 分钟. 来自:https://www.cnblogs.com/xiaohuiduan/p/12398775.html 首先 ...

  2. 一篇搞懂Java多线程运行机制

    文章目录 前言 线程 并发和并行 Runnable接口 Thread类 线程的创建 原理和注意事项 为什么不直接调用run方法? 线程终止和中断 用户线程和守护线程 代码演示 前言 Java是一种支持 ...

  3. 5张图搞懂Java深浅拷贝

    微信搜一搜 「bigsai」 关注这个专注于Java和数据结构与算法的铁铁 文章收录在github/bigsai-algorithm 欢迎star收藏 如果本篇对你有帮助,记得点赞收藏哦! 在开发.刷 ...

  4. 搞懂 Java HashMap 源码

    HashMap 源码分析 前几篇分析了 ArrayList , LinkedList ,Vector ,Stack List 集合的源码,Java 容器除了包含 List 集合外还包含着 Set 和 ...

  5. java 内部类定于_搞懂 JAVA 内部类

    前些天写了一篇关于 2018 年奋斗计划的文章,其实做 Android 开发也有一段时间了,文章中所写的内容,也都是在日常开发中遇到各种问题后总结下来需要巩固的基础或者进阶知识.那么本文就从内部类开刀 ...

  6. java 自旋锁_搞懂Java中的自旋锁

    轻松搞懂Java中的自旋锁 前言 在之前的文章<一文彻底搞懂面试中常问的各种"锁">中介绍了Java中的各种"锁",可能对于不是很了解这些概念的同学 ...

  7. java rest 序列化_一文看懂Java序列化

    一文看懂Java序列化 简介 首先我们看一下wiki上面对于序列化的解释. 序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓 ...

  8. ChatGPT似乎有的时候并不能搞懂Java的动态分派,你懂了吗?

    目录 碎碎念 ChatGPT 中出现的问题 那么正确答案应该是什么呢? 分派的相关知识点总结: 分派是什么? 静态分派与动态分派: Java语言是静态多分派,动态单分派的: 静态分派:静态重载多分派: ...

  9. python 类-Python入门--一篇搞懂什么是类

    原标题:Python入门--一篇搞懂什么是类 写一篇Python类的入门文章,在高级编程语言中,明白类的概念和懂得如何运用是必不可少的.文章有点长,3000多字. Python是面向对象的高级编程语言 ...

最新文章

  1. 可以卸载什么程序来对计算机进行瘦身,电脑越来越卡了,教你一分钟让电脑瘦身(C盘哪些文件可以删除)-怎么清理电脑内存...
  2. linux下安装nginx、python、django
  3. MacOS下安装BeautifulSoup库及使用
  4. Python编写俄罗斯方块小游戏
  5. 从零开始的网站搭建,服务器与域名管理
  6. 合理设置MTU,提升下载速度
  7. 【光学】基于matlab介电常数计算【含Matlab源码 1926期】
  8. 忘记密码怎么卸载和删除网维大师~
  9. Zoople HTML 编辑器组件 for NET
  10. 使用Pyecharts进行全国水质TDS地图可视化全过程8:绘制中国地图,使用timeline把多个值放在一个地图上
  11. 上海交通大学2004年数学分析考研试题
  12. python中字典的定义和操作
  13. 聊聊Ping命令的返回值Pong
  14. 判断BIOS的启动模式和磁盘分区格式
  15. Ubuntu安装Nixnote,解决印象笔记(国服/中国区)登录不能显示输入密码的问题
  16. Detecting Holes in Point Set Surfaces 笔记
  17. verilog二分频代码verilog三分频代码
  18. 安装卸载Oracle
  19. 2.2.2同向放大器、同向放大器的设计
  20. vue postman使用方法

热门文章

  1. “码绘” —— 使用P5.JS制作一幅自画像
  2. linux创建用户、设置密码
  3. 【Linux】Linux 用户和用户组管理
  4. 截面DID如何构造相关论文与代码实现
  5. Illustrator教你如果画人物插画
  6. 计算机的五层结构(网络湿人版)
  7. vivado报错信息学习过程更新
  8. js inArray() jQuery.inArray( value, array )
  9. 877数据结构与算法分析
  10. npm安装指定版本包,npm按版本安装;npm删除依赖,卸载依赖