有些时候,开发者想把程序运行过程中的数据临时保存到文件,可是前面介绍的字符流和字节流,要么用来读写文本字符串,要么用来读写字节数组,并不能直接保存某个对象信息,因为对象里面包括成员属性和成员方法,单就属性而言,每个属性又有各自的数据类型及其具体数值,这些复杂的信息既不能通过字符串表达,也不能通过简单的字节数组表达。虽然现有手段不容易往文件中写入对象信息,但是该想法无疑极具吸引力,倘若能够自如地对文件读写某个对象数据,必定会给程序员的开发工作带来巨大便利,况且内存都能存放对象信息,为何磁盘反而无法存储对象了呢?
解决问题的关键在于需要给对象建立某种映射关系,磁盘文件固然只能存放字节形式的数据,但如果能将某对象进行有规则的排序操作,使之变成整齐有序的信息队列,那么程序即可按照规矩把对象转为可存储的字节数据。正所谓英雄所见略同,Java确实提供了类似的解题思路,把对象转成磁盘文件可识别数据的过程,Java称之为“序列化”;反过来,把磁盘文件内容转成内存中对象的过程,Java称之为“反序列化”。如同字符串与字节数组的相互转换那般,序列化与反序列化一起完成了内存对象和磁盘文件之间的转换操作。
若想让一个对象支持序列化与反序列化,得事先声明该对象的来源类是可序列化的,也就是命令来源类实现Serializable接口,这样程序才知道由该类创建而来的所有对象都支持序列化与反序列化。举个用户信息类的例子,基本的用户信息通常包括用户名、手机号和密码三个字段,再添加Serializable接口的实现,于是可序列化的用户信息类代码变成以下这般:

  1. //定义一个可序列化的用户信息类。实现Serializable接口表示当前类支持序列化
  2. public class UserInfo implements Serializable {
  3. private String name; // 用户名
  4. private String phone; // 手机号码
  5. private String password; // 密码
  6.  
  7. public UserInfo() {
  8. name = "";
  9. phone = "";
  10. password = "";
  11. }
  12.  
  13. // 以下省略各字段的get***/set***方法
  14. }

之后来自于UserInfo的用户对象们纷纷摇身变为结构清晰的实例,不过由于序列化后的对象是种特殊的数据,因此还需专门的输入输出流进行处理。读写序列化对象的专用I/O流包括对象输入流ObjectInputStream和对象输出流ObjectOutputStream,其中前者用来从文件中读取对象信息,它的readObject方法完成了读对象操作;后者用来将对象信息写入文件,它的writeObject方法完成了写对象操作。下面是利用ObjectOutputStream往文件写入序列化对象的代码例子:

  1. private static String mFileName = "D:/test/user.txt";
  2. // 利用对象输出流把序列化对象写入文件
  3. private static void writeObject() {
  4. // 下面创建可序列化的用户信息对象,并给予赋值
  5. UserInfo user = new UserInfo();
  6. user.setName("王五");
  7. user.setPhone("15960238696");
  8. user.setPassword("111111");
  9. // 根据指定文件路径构建文件输出流对象,然后据此构建对象输出流对象
  10. try (FileOutputStream fos = new FileOutputStream(mFileName);
  11. ObjectOutputStream oos = new ObjectOutputStream(fos);) {
  12. oos.writeObject(user); // 把对象信息写入文件
  13. System.out.println("对象序列化成功");
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. }

由此可见,将对象信息写入文件的代码还是蛮简单的,从文件读取对象信息也很容易,只要下面的寥寥几行代码就搞定了:

  1. // 利用对象输入流从文件中读取序列化对象
  2. private static void readObject() {
  3. // 创建可序列化的用户信息对象
  4. UserInfo user = new UserInfo();
  5. // 根据指定文件路径构建文件输入流对象,然后据此构建对象输入流对象
  6. try (FileInputStream fos = new FileInputStream(mFileName);
  7. ObjectInputStream ois = new ObjectInputStream(fos);) {
  8. user = (UserInfo) ois.readObject(); // 从文件读取对象信息
  9. System.out.println("对象反序列化成功");
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. }
  13. // 注意用户信息的密码字段设置了禁止序列化,故而文件读到的密码字段为空
  14. String desc = String.format("姓名=%s,手机号=%s,密码=%s",
  15. user.getName(), user.getPhone(), user.getPassword());
  16. System.out.println("用户信息如下:"+desc);
  17. }

然后运行上述的对象数据读写代码,观察到下列的日志信息:

  1. 对象序列化成功
  2. 对象反序列化成功
  3. 用户信息如下:姓名=王五,手机号=15960238696,密码=111111

看到这些日志,有没有发现什么不对劲的地方?也许有人猛然惊醒,密码这么重要的字段居然会从文件里读到了明文?赶紧找到示例代码中的磁盘文件user.txt,使用文本编辑软件如UEStudio打开user.txt,在该文件末尾附近赫然出现了六位数字密码111111,详见下图所示的右下角。

显然密码值不应保存在文件里面,尤其是光天化日之下也能看到的明文。可见对象序列化应当有所取舍,寻常字段允许序列化,而私密字段不允许序列化。为此Java新增了关键字transient,凡是被transient修饰的字段,会在序列化之时自动予以屏蔽,也就是说,序列化无法保存该字段的数值。如此一来,用户信息UserInfo的类定义需要把password密码字段的声明代码改成下面这样:

  1. // 关键字transient可让它所修饰的字段无法序列化,也就是说,序列化无法保存该字段的数值
  2. private transient String password; // 密码

给密码字段添加了transient修饰之后,重新运行对象数据读写代码,根据下列的日志信息可知密码值已经屏蔽了序列化:

  1. 对象序列化成功
  2. 对象反序列化成功
  3. 用户信息如下:姓名=王五,手机号=15960238696,密码=null

另外,UserInfo类后续可能会增加新的成员属性,比如整型的年龄字段。然而一旦在UserInfo的代码定义中增加了新字段,再去读取原先保存在文件中的序列化对象,程序运行时竟然扔出异常,提示“java.io.InvalidClassException: com.io.bio.UserInfo; local class incompatible: stream classdesc serialVersionUID = ***, local class serialVersionUID = ***”,意思是本地类不兼容,IO流中的序列化编码与本地类的序列化编码不一致。其中的缘由说来话长,对象的每次序列化都需要一个编码serialVersionUID,程序通过该编码来校验读到的对象是否为原先的对象类型,而默认的编码数值是根据类名、接口名、成员方法及成员属性等联合运算得到的哈希值,所以只要类名、接口名、方法与属性任何一项发生变更,都会导致serialVersionUID编码产生变化,进而影响正常的序列化和反序列化操作。

这个序列化编码的校验规则,像极了Java版本的刻舟求剑,每次序列化的小船出发之前,都要在落剑的船身处做个标记,表示刚才宝剑是在该位置掉进水里的。其后小船的状态发生了改变,譬如开到了河对岸,此时船员开始活动筋骨,准备在标记处跳下船,意图潜水寻回宝剑。结果当然是徒劳无功,根本找不到先前落水的宝剑,因为标记刻在船身上,它跟随着小船运动,水里的剑未动而船已动,按照移动后的标记去找留在原地的宝剑,自然是竹篮打水一场空了。正确的做法是记下固定不动的方位信息,例如详细的经纬度,这样无论船怎么开,落剑的位置都是不变的。如此一来,还需在UserInfo的定义代码中添加以下的serialVersionUID赋值语句,从一开始就设置固定的版本编码数值:

  1. // 该类的实例在序列化时的版本编码
  2. private static final long serialVersionUID = 1L;

总结一下,支持序列化的类定义与普通的类定义主要有下述三项区别:
1、可序列化的类实现了Serializable接口;
2、可序列化的类需要给serialVersionUID字段赋值,避免出现版本编码不一致的情况;
3、可序列化的类可能有部分字段被关键字transient所修饰,表示这些字段无需进行序列化;
最后整合上述的三点要求,重新修改用户信息的类定义,改后的UserInfo代码片段示例如下:

  1. //定义一个可序列化的用户信息类。实现Serializable接口表示当前类支持序列化
  2. public class UserInfo implements Serializable {
  3. // 该类的实例在序列化时的版本编码
  4. private static final long serialVersionUID = 1L;
  5.  
  6. private String name; // 用户名
  7. private String phone; // 手机号码
  8. // 关键字transient可让它所修饰的字段无法序列化,也就是说,序列化无法保存该字段的数值
  9. private transient String password; // 密码
  10.  
  11. public UserInfo() {
  12. name = "";
  13. phone = "";
  14. password = "";
  15. }
  16.  
  17. // 以下省略各字段的get***/set***方法
  18. }

  

更多Java技术文章参见《Java开发笔记(序)章节目录

Java开发笔记(九十)对象序列化及其读写的更多相关文章

  1. java学习笔记之对象序列化

    1.简述 java对象序列化就是将对象编程二进制数据流的一种方法,从而实现对对象的传输和存储 2.作用 java是门面向对象编程语言,即一切皆对象,但是java对象只能存在于jvm中,一旦jvm停掉那 ...

  2. Java开发笔记(八十七)随机访问文件的读写

    前面介绍了字符流读写文件的两种方式,包括文件字符流和缓存字符流,但是它们的写操作都存在一个问题:不管是write方法还是append方法,都只能从文件开头写入,而不能追加到文件末尾或者在文件中间某个位 ...

  3. Java开发笔记(八十六)通过缓冲区读写文件

    前面介绍了利用文件写入器和文件读取器来读写文件,因为FileWriter与FileReader读写的数据以字符为单位,所以这种读写文件的方式被称作“字符流I/O”,其中字母I代表输入Input,字母O ...

  4. Java开发笔记(八十五)通过字符流读写文件

    前面介绍了文件的信息获取.管理操作,以及目录下的文件遍历,那么文件内部数据又是怎样读写的呢?这正是本文所要阐述的内容.File工具固然强大,但它并不能直接读写文件,而要借助于其它工具方能开展读写操作. ...

  5. Java开发笔记(九十四)文件通道的性能优势

    前面介绍了字节缓存的一堆概念,可能有的朋友还来不及消化,虽然文件通道的用法比起传统I/O有所简化,可是平白多了个操控繁琐的字节缓存,分明比较传统I/O更加复杂了.尽管字节缓存享有缓存方面的性能优势,但 ...

  6. Java开发笔记(九十三)深入理解字节缓存

    前面介绍了文件通道的读写操作,其中用到字节缓存ByteBuffer,它是位于通道内部的存储空间,也是通道唯一可用的存储形式.ByteBuffer有两种构建方式,一种是调用静态方法wrap,根据输入的字 ...

  7. Java开发笔记(九十二)文件通道的基本用法

    前面介绍的各色流式IO在功能方面着实强大,处理文件的时候该具备的操作应有尽有,可流式IO在性能方面不尽如人意,它的设计原理使得实际运行效率偏低,为此从Java4开始增加了NIO技术,通过全新的架构体系 ...

  8. Java开发笔记(五十二)对象的类型检查

    前面介绍了类的多态性,来自于鸡类的实例chicken,既能用来表达公鸡实例,也能用来表达母鸡实例.可是这导致了一个问题,假如在call方法内部需要手工判断输入参数属于公鸡实例还是母鸡实例,那该如何是好 ...

  9. Java开发笔记(九十六)线程的基本用法

    每启动一个程序,操作系统的内存中通常会驻留该程序的一个进程,进程包含了程序的完整代码逻辑.一旦程序退出,进程也就随之结束:反之,一旦强行结束进程,程序也会跟着退出.普通的程序代码是从上往下执行的,遇到 ...

随机推荐

  1. Flask入门之flask-wtf表单处理

    参考文章 1. 使用 WTForms 进行表单验证  第11集 #Sample.py # coding:utf-8 from flask import Flask,render_template,re ...

  2. 「速成应用」实在可靠的 微信小程序第三方代理加盟平台公司

    小程序,是基于微信平台的一个划时代产品,也就是嵌入到微信里的一个功能丰富.操作简洁的轻应用,不需要下载安装即可使用.不同的小程序,能实现不同的功能.例如,买电影票.餐厅排号.餐馆点菜.查询公交.查询股 ...

  3. 用 Python 鉴别色色的图片

    0 前言 实话实说啊,这个标题起得就有点标题党,识别是识别,准确率就有点玄学了. 1 环境说明 Win10 系统下 Python3,编译器是 Pycharm,需要安装 nonude 这个库. Pych ...

  4. Alfred效率神器

    下图就是Alfred的主界面我们所有的操作都在这一个界面上进行.通过热键打开主界面(本人设置的是option+command),输入一个"a"后Alfred就会为我在候选界面上显示 ...

  5. RestTemplate的设置及使用

    概述 RestTemplate是spring内置的http请求封装,在使用spring的情况下,http请求直接使用RestTemplate是不错的选择. Rest服务端 使用RestTemplate ...

  6. 传统IT公司/创业公司/互联网大公司的offer如何选择?[转载+原创]

    背景介绍: 第一家工作的公司是一家跨国外企安全公司, 骄傲的称自己不是互联网公司而是传统软件公司, 第二家公司是当下最热的知识分享社区, 创业公司. 第三家公司是挤走谷歌, 曾一度称霸中国的搜索引擎公 ...

  7. Linux时间子系统之(六):POSIX timer

    专题文档汇总目录 Notes:首先讲解了POSIX timer的标识(唯一识别).POSIX Timer的组织(管理POSIX Timer).内核中如何抽象POSIX Timer:然后分析了POSIX ...

  8. 代码方式设置WordPress内所有URL链接都在新标签页打开

    本文由荒原之梦原创,原文链接:http://zhaokaifeng.com/?p=699 前言: WordPress默认情况下几乎所有URL链接都是在同一个标签页打开.这样的话,读者点击一个链接就会离 ...

  9. 在Python中用Request库模拟登录(四):哔哩哔哩(有加密,有验证码)

    !已失效! 抓包分析 获取验证码 获取加密公钥 其中hash是变化的,公钥key不变 登录 其中用户名没有被加密,密码被加密. 因为在获取公钥的时候同时返回了一个hash值,推测此hash值与密码加密 ...

  10. search_request.go

    package types type SearchRequest struct {     // 搜索的短语(必须是UTF-8格式),会被分词     // 当值为空字符串时关键词会从下面的Token ...