深入理解Java原生的序列化机制

 更新时间:2019年06月06日 10:22:40   作者:mayoi7  
Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。下面小编和大家来一起学习一下吧

概念

一个对象如果想在硬盘上存储,一定就需要借助于一定的数据格式。这种把对象转换为硬盘存储的格式的过程就叫做对象的序列化,同样地,将这些文件再反向转换为程序中对象的操作就叫做反序列化
一些复杂的解决方案可能是将对象转换为json字符串的方式,这种方式的优点是易读,但是效率还是太低,所以Java的序列化的解决方案是将对象转换为一个二进制流的形式,来实现数据的持久化,本篇文章将会来详细讲解序列化的实现和原理

实现

准备

我们这里有一个普通的对象,要注意的是这个类和其中用到的所有对象都需要实现序列化接口Serializable:

class Demo implements Serializable {
int val = 10;
String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
A a = new A(20);
@Override
public String toString() {
return "[hashcode=" + hashCode() + " val=" + val + ", time=" + time 
+ ", A.val=" + a.val +"]";
}
}

这个A是一个普通的对象,如下:

class A implements Serializable {
int val = 20;
public A(int val) {
this.val = val;
}
}

现在我们有一个Demo对象,来输出一下这个对象的标志字符串:

Demo demo = new Demo();
System.out.println(demo.toString());

输出结果:

[hashcode=1625635731 val=10, time=20:28:56, A.val=20]

序列化

现在,我们需要将这个对象序列化为二进制流,则需要以下的操作:

FileOutputStream fileOutputStream = new FileOutputStream("target");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(demo);
objectOutputStream.flush();
objectOutputStream.close();

这样,demo对象就被我们持久化到硬盘的target文件中了

反序列化

反之,如果我们想将这个对象从target文件中取出,就需要如下的操作:

FileInputStream fileInputStream = new FileInputStream("target");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Demo newDemo = (Demo)objectInputStream.readObject();

检验

现在,我们用以下的语句来检验这两个对象是否是一个对象:

System.out.println(newDemo.toString());
System.out.println("demo == newDemo : " + (demo == newDemo));

输出

[hashcode=885284298 val=10, time=20:28:56, A.val=20]
demo == newDemo : false

我们会发现,反序列化得到的对象虽然值和原有对象一致,但是其不是同一个对象,这一点很重要

原理

我们打开序列化生成的target文件,这里需要用二进制流的方式打开:

这里可以将文件分为5个部分:

  • 文件头:声明文件是一个对象序列化文件,同时声明了序列化版本
  • 类描述:声明类信息,包括类名、序列化id,以及域的个数等属性
  • 属性描述
  • 父类信息描述
  • 对象属性的实际值

也就是说,在这个二进制文件中,通过这几部分就能表明一个类的全部信息,在反序列化的过程中,Java将会按照指定的文件格式来从文件中恢复数据

注意事项

序列化的类一定要实现Serializable接口
序列化类中包含的自定义对象都需要实现Serializable接口

这两点是为什么呢,我们来看ObjectOutputStream中的writeObject0方法,这里截取了一小段:

if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}

这段代码中的obj不仅仅是被序列化的对象,还会是这个对象中的所有字段,也就是说其中的域对象,必须是字符串、数组、枚举和序列化接口中的一种,否则就会抛出异常

序列化ID

其实,还有一点注意事项,我留在了这里来讲:

在序列化和反序列化之间,对象的字段名称、类型和数量均不能改变

这是为什么呢,我们来看反序列化中的一块代码:

if (model.serializable == osc.serializable &&
!cl.isArray() &&
suid != osc.getSerialVersionUID()) {
throw new InvalidClassException(osc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " +
osc.getSerialVersionUID());
}

这是ObjectStreamClass中的initNonProxy方法中的一段,这个方法也就是读取我们序列化文件的核心方法,用于初始化类描述符

不过我们重点不在这里,重点是一个suid和osc.getSerialVersionUID()的比较,这时候就要涉及到一个序列化id的概念了,序列化id的声明类似下面这种形式:

class Demo implements Serializable {
// 这个序列化id一般的ide都会提供有自动生成的插件,感兴趣的可以自行下载
private static final long serialVersionUID = -5809782578272943999L;
// ...
}

Java的反序列化成功与否的关键,就是比较文件的序列化id和类的序列化id是否一致,如果一致,则认为文件中的对象和类对象是同一个对象,否则,就说明两个类压根就不是一个类,如果强行转换则很有可能发生异常

但是我们之前没有手动设置序列化id也一样能反序列化成功不是吗?其实,之前能反序列化成功仅仅是因为我们没有改动原来的类,如果我们没有设置序列化id,则以下任何的操作,均会导致反序列化失败:

  • 修改了字段/方法的名称/类型
  • 添加或删除字段/方法

看到了吗,即使我们仅仅修改了字段的名称,也会导致反序列化的失败,如果不注意这一点,将会导致所有反序列化操作的崩溃,但是只要我们设置一个序列化id,即使我们把类中元素删的一干二净,也一样会反序列化成功,只不过是丢失属性而已

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • Spring Data Elasticsearch 5.x实现单词纠错和自动补全

    Spring Data Elasticsearch 5.x实现单词纠错和自动补全

    这篇文章主要为大家介绍了Spring Data Elasticsearch 5.x实现单词纠错和自动补全示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • Java多线程之显示锁和内置锁总结详解

    Java多线程之显示锁和内置锁总结详解

    这篇文章主要介绍了Java多线程之显示锁和内置锁总结详解,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11
  • kaptcha验证码组件使用简介解析

    kaptcha验证码组件使用简介解析

    这篇文章主要介绍了kaptcha验证码组件使用简介解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-08-08
  • 详解Java去除json数据中的null空值问题

    详解Java去除json数据中的null空值问题

    这篇文章主要介绍了详解Java去除json数据中的null空值问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • 复选框和Struts2后台交互代码详解

    复选框和Struts2后台交互代码详解

    这篇文章主要介绍了复选框和Struts2后台交互代码详解,分享了相关代码示例,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下
    2018-02-02
  • spring-@Autowired注入与构造函数注入使用方式

    spring-@Autowired注入与构造函数注入使用方式

    这篇文章主要介绍了spring-@Autowired注入与构造函数注入使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Spring Boot打包war jar 部署tomcat

    Spring Boot打包war jar 部署tomcat

    这篇文章主要介绍了Spring Boot打包war jar 部署tomcat的相关资料,需要的朋友可以参考下
    2017-10-10
  • SpringBoot集成RocketMQ实现消息发送的三种方式

    SpringBoot集成RocketMQ实现消息发送的三种方式

    RocketMQ 支持3 种消息发送方式: 同步 (sync)、异步(async)、单向(oneway),本文就将给大家介绍一下SpringBoot集成RocketMQ实现消息发送的三种方式文中有详细的代码示例,需要的朋友可以参考下
    2023-09-09
  • Java8学习教程之lambda表达式语法介绍

    Java8学习教程之lambda表达式语法介绍

    众所周知lambda表达式是JAVA8中提供的一种新的特性,它支持Java也能进行简单的“函数式编程”。 下面这篇文章主要给大家介绍了关于Java8学习教程之lambda表达式语法的相关资料,需要的朋友可以参考下。
    2017-09-09
  • jenkins按模块进行构建遇到的问题及解决方案

    jenkins按模块进行构建遇到的问题及解决方案

    这篇文章主要介绍了jenkins按模块进行构建的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-05-05

最新评论