一文看懂Java序列化

简介

首先我们看一下wiki上面对于序列化的解释。

序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

以最简单的方式来说,序列化就是将内存中的对象变成网络或则磁盘中的文件。而反序列化就是将文件变成内存中的对象。(emm,序列化就是将脑海中的“老婆”变成纸片人?反序列化就是将纸片人变成脑海中的“老婆”?当我没说)如果说的代码中具体一点,序列化就是将对象变成字节,而反序列化就是将字节恢复成对象。

当然,你在一个平台进行序列化,在另外一个平台也可以进行反序列化。

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

1. 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;(比如说服务器上用户的session对象)

2. 在网络上传送对象的字节序列。(比如说进行网络通信,消息(可以是文件)肯定要变成二进制序列才能在网络上面进行传输)

OK,既然我们已经了解到什么是(反)序列化了,那么多说无益,让我们来好好的看一看Java是怎么实现的吧。

Java实现

对于Java这把轻机枪来说,既然序列化是一个很重要的部分,那么它肯定自身提供了序列化的方案。

在Java中,只有实现了Serializable和Externalizable接口的类的对象才能够进行序列化。在下面将分别对两者进行介绍。

Serializable

最基本情况

Serializable可以说是最简单的序列化实现方案了。它就是一个接口,里面没有任何的属性和方法。一个类通过implements Serializable标示着这个类是可序列化的。下面将举一个简单的例子:

public class People implements Serializable{

private String name;

private int age;

public People(String name, int age){

this.name = name;

this.age = age;

}

@Override

public String toString(){

return "People{" +

"name='" + name + '\'' +

", age=" + age +

'}';

}

}

People类显而易见,是可序列化的。那么我们如何来实现可序列化呢?在序列化的过程中,有两个步骤:

序列化

创建一个ObjectOutputStream输出流。

调用ObjectOutputStream的writeObject函数输出可序列化的对象。

public class Main{

public static void main(String[] args) throws IOException{

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

People people = new People("name", 18);

oos.writeObject(people);

}

}

ObjectOutputStream对象中需要一个输出流,这里使用的是文件输出流(也可以是用其他输出流,例如System.out,输出到控制台)。然后我们通过调用writeObject就可以讲people对象写入到“object.txt”了。

反序列化

我们重新编辑People的构造方法,在里面添加一个输出来查看反序列化是否会进行调用构造函数。

public class People implements Serializable{

private String name;

private int age;

public People(String name, int age){

System.out.println("是否调用序列化?");

this.name = name;

this.age = age;

}

@Override

public String toString(){

return "People{" +

"name='" + name + '\'' +

", age=" + age +

'}';

}

}

反序列化和序列化一样,也分为2个步骤:

创建一个ObjectInputStream输入流

调用ObjectInputStream中的readObject函数得到序列化的对象

public class Main{

public static void main(String[] args) throws IOException, ClassNotFoundException{

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

People people = (People) ois.readObject();

System.out.println(people);

}

}

下面是程序运行之后的控制台的图片。

可以很明显的看见,反序列化的时候,并没有调用People的构造方法。反序列化的对象是由JVM自己生成的对象,而不是通过构造方法生成。

Ok,通过上面我们简单的学会了序列化的使用,那么,我们会有一个问题,一个对象在序列化的过程中,有哪一些属性是可是序列化的,哪一些是不可序列化的呢?

通过查看源代码,我们可以知道:

对象的类,签名和非transient和非static变量会写入到类中。

类的成员为引用

看到很多博客都是这样说的:

如果一个可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。

其实这样说不是很准确,因为即使是String类型,里面也实现了Serializable这个接口。

我们新建一个Man类,但是它并没有实现Serializable方法。

public class Man{

private String sex;

public Man(String sex){

this.sex = sex;

}

@Override

public String toString(){

return "Man{" +

"sex='" + sex + '\'' +

'}';

}

}

然后在People类中进行引用。

public class People implements Serializable{

private String name;

private int age;

private Man man;

@Override

public String toString(){

return "People{" +

"name='" + name + '\'' +

", age=" + age +

", man=" + man +

'}';

}

public People(String name, int age, Man man){

this.name = name;

this.age = age;

this.man = man;

}

}

如果我们进行序列化,会发生以下错误:

java.io.NotSerializableException: People

at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

at Main.main(Main.java:41)

因为Man是不可序列化的,也就导致了People类是不可序列化的。

同一对象多次序列化

大家看一下下面的这段代码:

public class Main{

public static void main(String[] args) throws IOException, ClassNotFoundException{

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

People people = new People("name", 11);

oos.writeObject(people);

oos.writeObject(people);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

People people1 = (People) ois.readObject();

People people2 = (People) ois.readObject();

System.out.println(people1 == people2);

}

}

你们觉得会输出啥?

最后的结果会输出true。

然后大家再看一段代码,与上面代码不同的是,People在第二次writeObject的时候,对name进行了重新赋值操作。

public class Main{

public static void main(String[] args) throws IOException, ClassNotFoundException{

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

People people = new People("name", 11);

oos.writeObject(people);

people.setName("hello");

oos.writeObject(people);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

People people1 = (People) ois.readObject();

People people2 = (People) ois.readObject();

System.out.println(people1 == people2);

}

}

结果会输出啥?

结果还是:true,同时在people1和people2对象中,name都为“name”,而不是为“hello”。

why??为什么会这样?

在默认情况下,对于一个实例的多个引用,为了节省空间,只会写入一次。而当写入多次时,只会在后面追加几个字节而已(代表某个实例的引用)。

但是我们如果向在后面追加实例而不是引用那么我们应该怎么做?使用rest或writeUnshared即可。

public class Main{

public static void main(String[] args) throws IOException, ClassNotFoundException{

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

People people = new People("name", 11);

oos.writeObject(people);

people.setName("hello");

oos.reset();

oos.writeObject(people);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

People people1 = (People) ois.readObject();

People people2 = (People) ois.readObject();

System.out.println(people1);

System.out.println(people2);

System.out.println(people1 == people2);

}

}

public class Main{

public static void main(String[] args) throws IOException, ClassNotFoundException{

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

People people = new People("name", 11);

oos.writeObject(people);

people.setName("hello");

oos.writeUnshared(people);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

People people1 = (People) ois.readObject();

People people2 = (People) ois.readObject();

System.out.println(people1);

System.out.println(people2);

System.out.println(people1 == people2);

}

}

子父类引用序列化

子类和父类有两种情况:

子类没有序列化,父类进行了序列化

子类进行序列化,父类没有进行序列化

emm,第一种情况不需要考虑,肯定不会出错。让我们来看一看第二种情况会怎么样!!

父类Man类

public class Man{

private String sex;

public Man(String sex){

this.sex = sex;

}

@Override

public String toString(){

return "Man{" +

"sex='" + sex + '\'' +

'}';

}

}

子类People类:

public class People extends Man implements Serializable{

private String name;

private int age;

public People(String name, int age, String sex){

super(sex);

this.name = name;

this.age = age;

}

@Override

public String toString(){

return "People{" +

"name='" + name + '\'' +

", age=" + age +

"} " + super.toString();

}

}

如果这个时候,我们对People进行序列化会怎么样呢?会报错!!

Exception in thread "main" java.io.InvalidClassException: People; no valid constructor

at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169)

at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874)

at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2098)

at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1625)

at java.io.ObjectInputStream.readObject(ObjectInputStream.java:465)

at java.io.ObjectInputStream.readObject(ObjectInputStream.java:423)

at Main.main(Main.java:38)

如何解决,我们可以在Man中,添加一个无参构造器即可。这是因为当父类不可序列化的时候,需要调用默认无参构造器初始化属性的值。

可自定义的可序列化

我们会有一个疑问,序列化可以将对象保存在磁盘或者网络中,but,我们如何能够保证这个序列化的文件的不会被被人查看到里面的内容。假如我们在进行序列化的时候就像这些属性进行加密不就Ok了吗?(这个仅仅是举一个例子)

可自定义的可序列化有两种情况:

某些变量不进行序列化

在序列化的时候改变某些变量

在上面我们知道transient和static的变量不会进行序列化,因此我们可以使用transient来标记某一个变量来限制它的序列化。

在第二中情况我们可以通过重写writeObject与readObject方法来选择对属性的操作。(还有writeReplace和readResolve)

在下面的代码中,通过transient来限制name写入,通过writeObject和readObject来对写入的age进行修改。

public class People implements Serializable{

transient private String name;

private int age;

public People(String name, int age){

this.name = name;

this.age = age;

}

private void writeObject(ObjectOutputStream out) throws IOException{

out.writeInt(age + 1);

}

private void readObject(ObjectInputStream in) throws IOException{

this.age = in.readInt() -1 ;

}

}

至于main函数怎么调用?还是正常的调用:

public class Main{

public static void main(String[] args) throws IOException, ClassNotFoundException{

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

People people = new People("name", 11);

oos.writeObject(people);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

People people1 = (People) ois.readObject();

}

}

Externalizable:强制自定义序列化

这个,emm,“强制”两个字都懂吧。让我们来看一看这个接口的源代码:

public interface Externalizable extends java.io.Serializable{

void writeExternal(ObjectOutput out) throws IOException;

void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

}

简单点来说,就是类通过implements这个接口,实现这两个方法来进行序列化的自定义。

public class People implements Externalizable{

private String name;

private int age;

public People(String name, int age){

this.name = name;

this.age = age;

}

// 注意必须要一个默认的构造方法

public People(){

}

public void writeExternal(ObjectOutput out) throws IOException{

out.writeInt(this.age+1);

}

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{

this.age = in.readInt() - 1;

}

}

两者之间的差异

方案实现Serializable接口实现Externalizable接口

方式

系统默认决定储存信息

程序员决定存储哪些信息

方法

使用简单,implements即可

必须实现接口内的两个方法

性能

性能略差

性能略好

序列化版本号serialVersionUID

我相信很多人都看到过serialVersionUID,随便打开一个类(这里是String类),我么可以看到:

/** use serialVersionUID from JDK 1.0.2 for interoperability */

private static final long serialVersionUID = -6849794470754667710L;

使用来自JDK 1.0.2 的serialVersionUID用来保持连贯性

这个serialVersionUID的作用很简单,就是代表一个版本。当进行反序列化的时候,如果class的版本号与序列化的时候不同,则会出现InvalidClassException异常。

版本好可以只有指定,但是有一个点要值得注意,JVM会根据类的信息自动算出一个版本号,如果你更改了类(比如说添加/修改了属性或者方法),则计算出来的版本号就发生了改变。这样也就代表这你无法反序列化你以前的东西。

什么情况下需要修改serialVersionUID呢?分三种情况。

修改了方法,这个当然版本好不需要改变

修改了静态变量或者transient关键之修饰的变量,同样不需要修改。

新增了变量或者删除了变量也不需要修改。如果是新增了变量,则进行反序列化的时候会给新增的变量赋一个默认值。如果是修改了变量,则进行反序列化的时候无需理会被删除的值。

讲完了讲完了,序列化实际上还是挺简单。不过需要注意使用的时候遇到的坑。~~

java rest 序列化_一文看懂Java序列化相关推荐

  1. java 序列化 uid_一文看懂Java序列化之serialVersionUID

    serialVersionUID适用于Java的序列化机制.简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的.在进行反序列化时,JVM会把传来的字节流中的 ...

  2. java arraylist排序_一文读懂Java集合框架

    欢迎关注微信公众号:深入浅出Java源码 概念 Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们.集合框架被设计成要满足以下几个目标. 该框架必须是高性能的.基本集合(动态数组,链表, ...

  3. java 医疗监护_一文看懂各类传感器在医疗监护领域的作用

    你睡得好吗?"晚上睡不着,早上起不来"已经成为困扰很多80后.90后的"大问题"!脱发.黑眼圈.工作时精神不济--都是失眠惹的祸! 刚刚过去的3月21日,是第1 ...

  4. java吵醒线程_一文搞懂 Java 线程中断

    在之前的一文<如何"优雅"地终止一个线程>中详细说明了 stop 终止线程的坏处及如何优雅地终止线程,那么还有别的可以终止线程的方法吗?答案是肯定的,它就是我们今天要分 ...

  5. java sleep方法_一文搞懂 Java 线程中断!

    在之前的一文<如何"优雅"地终止一个线程>详细说明了 stop 终止线程的坏处及如何优雅地终止线程,那么还有别的可以终止线程的方法吗?答案是肯定的,它就是我们今天要分享 ...

  6. java构造器 权限_一文搞懂Java的 构造方法 和 访问权限

    目录 零.前言 Java是一门当今最火的编程语言之一,拥有很多现成可用的库,在我们编程生涯中,有着无比重要的地位. Java中有个概念叫做访问权限.它们是什么呢?今天我来详细讲解. 本文所有代码已经上 ...

  7. java 委派关系_一文读懂java类加载之双亲委派机制

    一个编译后的class文件,想要在JVM中运行,就需要先加载到JVM中.java中将类的加载工具抽象为类加载器,而通过加载工具加载类文件的具体方式被称为双亲委派机制. 知识点 类加载器:通过一个类全限 ...

  8. java中date类型如何赋值_一文读懂java中的Reference和引用类型

    简介 java中有值类型也有引用类型,引用类型一般是针对于java中对象来说的,今天介绍一下java中的引用类型.java为引用类型专门定义了一个类叫做Reference.Reference是跟jav ...

  9. angular 字符串转换成数字_一文看懂Python列表、元组和字符串操作

    好文推荐,转自CSDN,原作星辰StarDust,感觉写的比自己清晰-大江狗荐语. 序列 序列是具有索引和切片能力的集合. 列表.元组和字符串具有通过索引访问某个具体的值,或通过切片返回一段切片的能力 ...

最新文章

  1. “轻雀”成就团队和个人,共建高效知识协同平台
  2. MVC模式与三层架构的区别
  3. linux如何退出python编辑器_centos linux 终端已经调成python3,怎么安装python3的IDLE编译器,...
  4. Jedis Cluster源码分析
  5. javaSE各阶段练习题--流程控制
  6. 如何处理使用 SAP UI5 消费真实的 OData 服务时遇到的跨域问题
  7. 小白学数据分析-----留存率分析_I[次日留存率突然下降了50%?]
  8. php mysql倒计时_php 倒计时程序
  9. UVa 12657 - Boxes in a Line ( 双向链表 )
  10. 修改USB固件库的Customer_HID例程
  11. Matplotlib 三维图像 API
  12. Android动态添加Fragment
  13. 小程序模仿通讯录制作
  14. 玻尔兹曼机(Boltzmann机)和深度置信网络基本概念
  15. 联想笔记本电脑BIOS中英文对照
  16. flink 时间语义、水位线(Watermark)、生成水位线、水位线的传递
  17. Sentinel-1A数据 GRD和SLC的区别
  18. 标书的参考格式及参考内容
  19. Visual Studio2022 运行代码时“发生生成错误,是否继续并运行上次的成功生成”
  20. 最基本的顺序表(经典顺序表)

热门文章

  1. 测序比对软件的总结----bowtie2
  2. 用Docker搭建Python环境
  3. node.js中mysql批量插入更新的三种方法
  4. 尚硅谷谷粒商城第一天 基础
  5. div搜索框与按钮不在一行_手机资源搜索神器+下载神器,结合起来用才真的爽...
  6. 【MyBatis】关于MyBatis批量更新的几种方式
  7. golang单元测试框架GoConvey
  8. 【MFC】一个最简单的MFC程序(9)
  9. Chrome Devtool 使用指南
  10. CF1542C Strange Function