序列化和反序列化

目录

1.背景知识

序列化的定义

为什么要序列化?

什么情况下需要序列化?

序列化的方式

序列化技术选型的几个关键点

2.JAVA序列化和反序列化

Java 是如何实现序列化的?

JAVA序列化中常见的问题

3.Python序列化和反序列化

4.跨语言序列化方式

JSON序列化

JSON进阶


1.背景知识

序列化的定义

序列化:把对象转化为可传输的字节序列过程称为序列化。

反序列化:把字节序列还原为对象的过程称为反序列化。


为什么要序列化?

如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来推导出什么是序列化, 那么究竟序列化的目的是什么?

其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组。

因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我们必须在把对象转成字节数组的时候就制定一种规则(序列化),那么我们从IO流里面读出数据的时候再以这种规则把对象还原回来(反序列化)。

如果我们要把一栋房子从一个地方运输到另一个地方去,序列化就是我把房子拆成一个个的砖块放到车子里,然后留下一张房子原来结构的图纸,反序列化就是我们把房子运输到了目的地以后,根据图纸把一块块砖头还原成房子原来面目的过程


什么情况下需要序列化?

通过上面我想你已经知道了凡是需要进行“跨平台存储”和”网络传输”的数据,都需要进行序列化。

本质上存储和网络传输 都需要经过 把一个对象状态保存成一种跨平台识别的字节格式,然后其他的平台才可以通过字节信息解析还原对象信息。


序列化的方式

序列化只是一种拆装组装对象的规则,那么这种规则肯定也可能有多种多样,比如现在常见的序列化方式有:

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


序列化技术选型的几个关键点

序列化协议各有千秋,不能简单的说一种序列化协议是最好的,只能从你的当时环境下去选择最适合你们的序列化协议,如果你要为你的公司项目进行序列化技术的选型,那么主要从以下几个因素。

协议是否支持跨平台

如果你们公司有好多种语言进行混合开发,那么就肯定不适合用有语言局限性的序列化协议,要不然你JDK序列化出来的格式,其他语言并没法支持。

序列化的速度

如果序列化的频率非常高,那么选择序列化速度快的协议会为你的系统性能提升不少。

序列化出来的大小

如果频繁的在网络中传输的数据那就需要数据越小越好,小的数据传输快,也不占带宽,也能整体提升系统的性能。


2.JAVA序列化和反序列化

原文-知乎 序列化理解起来很简单 - 知乎 (zhihu.com)

Java 是如何实现序列化的?

前面主要介绍了一下什么是序列化,那么下面主要讲下JAVA是如何进行序列化的,以及序列化的过程中需要注意的一些问题

  • 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对象就是一个序列化和反序列化的过程。

序列化原理推荐阅读:http://developer.51cto.com/art/200908/147650.htm


JAVA序列化中常见的问题

  • 问题一:static 属性不能被序列化

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

  • 问题二:Transient 属性不会被序列化

接着上面的案例,我们在User里面加上一个transient 状态的心情属性mood;

public class User implements Serializable {
 //年龄
 private int age;
 //名字
 private String name;
 //心情
 private transient String mood;
//省略get set方法
​
}

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

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
​
User user = new User();
user.setMood("愉快");
oos.writeObject(user);
​
oos.flush();
oos.close();

再把从文件读取出来的转换为对象并打印mood的值

FileInputStream fis = new FileInputStream("D:\\temp.txt");
​
ObjectInputStream oin = new ObjectInputStream(fis);
​
User user1 = (User) oin.readObject();
​
System.out.println("mood="+user1.getMood());

输出结果为:mood=null(原生类型为对应类型的默认值,包装类型为null)

  • 问题三:序列化版本号serialVersionUID

所有实现序列化的对象都必须要有个版本号,这个版本号可以由我们自己定义,当我们没定义的时候JDK工具会按照我们对象的属性生成一个对应的版本号。


版本号有什么用?

其实这个版本号就和我们平常软件的版本号一样,你的软件版本号和官方的服务器版本不一致的话就告诉你有新的功能更新了,主要用于提示用户进行更新。序列化也一样,我们的对象通常需要根据业务的需求变化要新增、修改或者删除一些属性,在我们做了一些修改后,就通过修改版本号告诉 反序列化的那一方对象有了修改你需要同步修改。

使用JDK生成的版本号和我们自定义的版本号的区别?

JDK工具生成的serialVersionUID 是根据对象的属性信息生成的一个编号,这就意味着只要对象的属性有一点变动那么他的序列化版本号就会同步进行改变。

这种情况有时候就不太友好,就像我们的软件一样,使用JDK生成的serialVersionUID,只要对象有一丁点改变serialVersionUID就会随着变更,这样的话用户就得强制更新软件的版本,用户不更新就使用不了软件。

而大多数友好的情况也许是这样的,用户可以选择不更新,不更新的话用户只是无法体验新加的功能而已。

而这种方式就需要我们自定义的版本号了,这样我就可以在新增了属性后不修改serialVersionUID,反序列化的时候只是无法或许新加的属性,并不影响程序运行。

下面用代码测试一下我们的理论:

(1)对象属性序列化版本号不同进行序列化和反序列化

继上面的例子

序列化之前我们设置serialVersionUID=2

public class User implements Serializable {
​
 private  static  final  long serialVersionUID=2;
 //年龄
 private int age;
 //名字
 private String name;
​
}

序列化存储User到temp.txt

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
​
User user = new User();
user.setName("sandy");
user.setAge(18);
oos.writeObject(user);
​
oos.flush();
oos.close();

然后我们反序列化的时候对象的版本号是serialVersionUID=1

public class User implements Serializable {
​
 private  static  final  long serialVersionUID=1;
 //年龄
 private int age;
 //名字
 private String name;
​
}

最后再把从文件数据反序列化取出来

FileInputStream fis = new FileInputStream("D:\\temp.txt");
​
ObjectInputStream oin = new ObjectInputStream(fis);
​
User user1 = (User) oin.readObject();
​
System.out.println("name="+user1.getName());

最后执行结果反序列化异常,原因是对象序列化和反序列化的版本号不同导致

序列化和反序列化

(2)对象新增属性,但是版本号相同也可以反序列化成功

序列化的对象信息 这里比反序列化的对象多了个SEX属性

public class User implements Serializable {
​
 private  static  final  long serialVersionUID=1;
 //年龄
 private int age;
 //名字
 private String name;
 //年龄
 private  int sex;
}

序列化存储User到temp.txt

FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
​
User user = new User();
user.setName("sandy");
user.setAge(18);
user.setSex("女");
oos.writeObject(user);
​
oos.flush();
oos.close();

反序列化的对象信息

序列化的对象信息 这里比序列化的对象少了个SEX属性,但版本号一致

public class User implements Serializable {
​
 private  static  final  long serialVersionUID=1;
 //年龄
 private int age;
 //名字
 private String name;
 }

最后再把从文件数据反序列化取出来

FileInputStream fis = new FileInputStream("D:\\temp.txt");
​
ObjectInputStream oin = new ObjectInputStream(fis);
​
User user1 = (User) oin.readObject();
​
System.out.println("name="+user1.getName());

最后控制台打印结果正常

序列化和反序列化

结果证明,只要序列化版本一样,对象新增属性并不会影响反序列化对象。

(3)对象新增属性,但是版本号是使用的JDK生成序列化版本号

省略代码,最后执行结果报错,原因是序列化和反序列化的版本号不一致造成。

序列化和反序列化

  • 问题四:父类、子类序列化问题

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

(1)父类没有实现序列化,子类实现序列化

父类

public class Parent {
 //爱好
 private String like;
}

子类

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

序列化后再反序列化

//序列化User对象存储到temp.txt
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
​
User user = new User();
user.setName("sandy");
user.setAge(18);
user.setLike("看美女");
oos.writeObject(user);
​
oos.flush();
oos.close();
​
//从temp.txt 反序列化转为User对象
FileInputStream fis = new FileInputStream("D:\\temp.txt");
​
ObjectInputStream oin = new ObjectInputStream(fis);
​
User user1 = (User) oin.readObject();
​
System.out.println("like="+user1.getLike());

最后执行结果,父类属性未被序列化

序列化和反序列化

(2)父类实现序列化,子类不实现序列化

父类

public class Parent implements Serializable{
 //爱好
 private String like;
​
​
}

子类

public class User extends Parent {
 //年龄
 private int age;
 //名字
 private String name;
​
​
}

序列化后再反序列化

//序列化User对象存储到temp.txt
FileOutputStream fos = new FileOutputStream("D:\\temp.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
​
User user = new User();
user.setName("sandy");
user.setAge(18);
user.setLike("看美女");
oos.writeObject(user);
​
oos.flush();
oos.close();
​
//从temp.txt 反序列化转为User对象
FileInputStream fis = new FileInputStream("D:\\temp.txt");
​
ObjectInputStream oin = new ObjectInputStream(fis);
​
User user1 = (User) oin.readObject();
​
System.out.println("name="+user1.getName());

最后执行结果,子类属性序列化正常

序列化和反序列化

参考资料

https://blog.csdn.net/frankarmstrong/article/details/54959727

http://developer.51cto.com/art

3.Python序列化和反序列化

在程序运行的过程中,所有的变量都是在内存中,比如,定义一个dict:

d = dict(name='Bob', age=20, score=88)

可以随时修改变量,比如把name改成'Bill',但是一旦程序结束,变量所占用的内存就被操作系统全部回收。如果没有把修改后的'Bill'存储到磁盘上,下次重新运行程序,变量又被初始化为'Bob'

我们把变量从内存中变成可存储或传输的过程称之为序列化,在Python中叫pickling,在其他语言中也被称之为serialization,marshalling,flattening等等,都是一个意思。

序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。

反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。

Python提供了pickle模块来实现序列化。

首先,我们尝试把一个对象序列化并写入文件:

>>> import pickle
>>> d = dict(name='Bob', age=20, score=88)
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x03\x00\x00\x00ageq\x01K\x14X\x05\x00\x00\x00scoreq\x02KXX\x04\x00\x00\x00nameq\x03X\x03\x00\x00\x00Bobq\x04u.'

pickle.dumps()方法把任意对象序列化成一个bytes,然后,就可以把这个bytes写入文件。或者用另一个方法pickle.dump()直接把对象序列化后写入一个file-like Object:

>>> f = open('dump.txt', 'wb')
>>> pickle.dump(d, f)
>>> f.close()

看看写入的dump.txt文件,一堆乱七八糟的内容,这些都是Python保存的对象内部信息。

当我们要把对象从磁盘读到内存时,可以先把内容读到一个bytes,然后用pickle.loads()方法反序列化出对象,也可以直接用pickle.load()方法从一个file-like Object中直接反序列化出对象。我们打开另一个Python命令行来反序列化刚才保存的对象:

>>> f = open('dump.txt', 'rb')
>>> d = pickle.load(f)
>>> f.close()
>>> d
{'age': 20, 'score': 88, 'name': 'Bob'}

变量的内容又回来了!

当然,这个变量和原来的变量是完全不相干的对象,它们只是内容相同而已。

Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。

4.跨语言序列化方式

JSON序列化

如果我们要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XML,但更好的方法是序列化为JSON,因为JSON表示出来就是一个字符串,可以被所有语言读取,也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在Web页面中读取,非常方便。

JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:

JSON类型 Python类型
{} dict
[] list
"string" str
1234.56 int或float
true/false True/False
null None

Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。我们先看看如何把Python对象变成一个JSON:

>>> import json
>>> d = dict(name='Bob', age=20, score=88)
>>> json.dumps(d)
'{"age": 20, "score": 88, "name": "Bob"}'

dumps()方法返回一个str,内容就是标准的JSON。类似的,dump()方法可以直接把JSON写入一个file-like Object

要把JSON反序列化为Python对象,用loads()或者对应的load()方法,前者把JSON的字符串反序列化,后者从file-like Object中读取字符串并反序列化:

>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
>>> json.loads(json_str)
{'age': 20, 'score': 88, 'name': 'Bob'}

由于JSON标准规定JSON编码是UTF-8,所以我们总是能正确地在Python的str与JSON的字符串之间转换。

JSON进阶

Python的dict对象可以直接序列化为JSON的{},不过,很多时候,我们更喜欢用class表示对象,比如定义Student类,然后序列化:

import json
​
class Student(object):
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score
​
s = Student('Bob', 20, 88)
print(json.dumps(s))

运行代码,毫不留情地得到一个TypeError

Traceback (most recent call last):
  ...
TypeError: <__main__.Student object at 0x10603cc50> is not JSON serializable

错误的原因是Student对象不是一个可序列化为JSON的对象。

如果连class的实例对象都无法序列化为JSON,这肯定不合理!

别急,我们仔细看看dumps()方法的参数列表,可以发现,除了第一个必须的obj参数外,dumps()方法还提供了一大堆的可选参数:

json — JSON encoder and decoder — Python 3.10.2 documentation

这些可选参数就是让我们来定制JSON序列化。前面的代码之所以无法把Student类实例序列化为JSON,是因为默认情况下,dumps()方法不知道如何将Student实例变为一个JSON的{}对象。

可选参数default就是把任意一个对象变成一个可序列为JSON的对象,我们只需要为Student专门写一个转换函数,再把函数传进去即可:

def student2dict(std):
    return {
        'name': std.name,
        'age': std.age,
        'score': std.score
    }

这样,Student实例首先被student2dict()函数转换成dict,然后再被顺利序列化为JSON:

>>> print(json.dumps(s, default=student2dict))
{"age": 20, "name": "Bob", "score": 88}

不过,下次如果遇到一个Teacher类的实例,照样无法序列化为JSON。我们可以偷个懒,把任意class的实例变为dict

print(json.dumps(s, default=lambda obj: obj.__dict__))

因为通常class的实例都有一个__dict__属性,它就是一个dict,用来存储实例变量。也有少数例外,比如定义了__slots__的class。

同样的道理,如果我们要把JSON反序列化为一个Student对象实例,loads()方法首先转换出一个dict对象,然后,我们传入的object_hook函数负责把dict转换为Student实例:

def dict2student(d):
    return Student(d['name'], d['age'], d['score'])

运行结果如下:

>>> json_str = '{"age": 20, "score": 88, "name": "Bob"}'
>>> print(json.loads(json_str, object_hook=dict2student))
<__main__.Student object at 0x10cd3c190>

打印出的是反序列化的Student实例对象。

上一篇:如何优雅地把大批手写实验数据输入电脑?


下一篇:python 读取json文件