14.序列化和文件的输入/输出 保存对象

14.1 保存对象状态

你已经奏出完美的乐章,现在会想把它储存起来。你可以抓个文房四宝把它记下来,但也可以按下储存按钮(或按下File菜单上的Save)。然后你帮文件命名,并希望这个文件不会让屏幕变成蓝色的画面。 

储存状态的选择有很多种,这可能要看你会如何使用储存下来的状态而决定。我们会在这一章讨论下面两种选项:

如果只有自己写的Java程序会用到这些数据:

① 用序列化(serialization)。
将被序列化的对象写到文件中。然后就可以让你的程序去文件中读取序列化的对象并把它们展开回到活生生的状态。

如果数据需要被其他程序引用:

② 写一个纯文本文件。用其他程序可以解析的特殊字符写到文件中。例如写成用tab字符来分隔的档案以便让电子表格或数据库应用程序能够应用。

当然还有其他的选择。你可以将数据存进任何格式中。举例来说,你可以把数据用字节而不是字符来写入,或者你也可以将数据写成Java的primitive主数据类型,有一些方法可以提供int、long、boolean等的写入功能。但不管用什么方法,基本所需的输入/输出技巧都一样:把数据写到某处,这可能是个磁盘上的文件,或者是来自网络上的串流。读取数据的方向则刚好相反。当然此处所讨论的部分不涉及使用数据库的情况。

存储状态

假设你有个程序,是个幻想冒险游戏,要过很多关才能完成。在游戏进行的过程中,游戏的人物会累积经验值、宝物、体力等。你不会想让游戏每次重新启动时都得要从头来过—这样根本没人玩。因此你需要一种方法来保存人物的状态,并且在重新开启时能够将状态回复到上次存储时的原状。因为你是程序员,所以你的工作是要让存储与恢复尽可能的简单容易。

① 选项一

把3种序列化的人物对象写入文件中。

创建一个文件,让序列化的3种对象写到此文件中。这文件在你以文本文件形式阅读时是无意义的:
“isr GameCharacter
“oge8iTpowerLjava/lang/
String;[weaponst[Ljava/lang/
String;xp&tlfur[Ljava.lang.String;-“vA应(Gxptbowtswordtdustsq~》tTrolluq~tbare handstbig axsq-xtMagicianuq-tspe llstinvisibility

② 选项二

写入纯文本文件

创建文件,写入3行文字,每个人物一行,以逗点来分开属性:
80,EIf,bow, sword,dust
800,Troll,bare hands,big ax
180,Magician,spells,invisibility

14.2 写入文件的序列化对象

将序列化对象写入文件

下面是将对象序列化(存储)的方法步骤:

1.创建出FileOutputStream 

FileOutputStream fileStream = new FileOutputStream("MyGame.eser");

2.创建ObjectOutputStream

ObjectOutputStream os = new ObjectOutputStream(fileStream);

3.写入对象

os.writeObject(characterOne);
os.writeObject(characterTwo);
os.writeObject(characterThree);

4.关闭ObjectOutputStream

os.close();

14.3 输入/输出串流

Java的输入/输出API带有连接类型的串流,它代表来源与目的地之间的连接,连接串流将串流与其他串流连接起来。

一般来说,串流要两两连接才能作出有意义的事情——其中一个表示连接,另一个则是要被调用方法的。为何要两个?因为连接的串流通常都是很低层的。以FileOutputStream为例,它有可以写入字节的方法。但我们通常不会直接写字节,而是以对象层次的观点来写入,所以需要高层的连接串流。

那又为何不以单一的串流来执行呢?这就要考虑到良好的面向对象设计了。每个类只要做好一件事。FileOutputStream把字节写入文件。ObjectOutputStream把对象转换成可以写入串流的数据。当我们调用ObjectOutputStream的writeObject时,对象会被打成串流送到FileOutputStream来写入文件。

这样就可以通过不同的组合来达到最大的适应性!如果只有一种串流类的话,你只好祈祷API的设计人已经想好所有可能的排列组合。但通过链接的方式,你可以自由地安排串流的组合与去向。

将串流(stream)连接起来代表来源与目的地(文件或网络端口)的连接。串流必须要连接到某处才能算是个串流。

14.4 对象序列化

对象被序列化的时候发生了什么事?

1.在堆上的对象

在堆上的对象有状态——实例变量的值。这些值让同一类的不同实例有不同的意义

2.被序列化的对象

序列化的对象保存了实例变量的值,因此之后可以在堆上带回一模一样的实例

对象的状态是什么?有什么需要保存?

存储primitive主数据类型值37和70是很简单的。但如果对象有引用到其他对象的实例变量时要怎么办?如果这些对象还带有其他对象又该如何?对象基本上有哪些部分是独特的?有哪些东西需要被带回来才能让对象回到和存储时完全相同的状态?当然它会有不同的内存位置,但这无关紧要。我们在乎的是堆上是否有与存储时一模一样的对象状态。

当对象被序列化时,被该对象引用的实例变量也会被序列化。且所有被引用的对象也会被序列化··最棒的是,这些操作都是自动进行的!

序列化程序会将对象版图上的所有东西存储起来。被对象的实例变量所引用的所有对象都会被序列化。

Kennel对象带有对Dog数组对象的引用。Dog[]中有两个Dog对象的引用。每个Dog对象带有String和Collar对象的引用。String对象维护字符的集合,而Collar对象持有一个int。
当保存kennel对象时,所有的对象都会保存! 

14.5 实现Serializable接口

如果要让类能够被序列化,就实现Serializable

Serializable接口又被称为marker或tag类的标记用接口,因为此接口并没有任何方法需要实现的。它的唯一目的就是声明有实现它的类是可以被序列化的。也就是说,此类型的对象可以通过序列化的机制来存储。如果某类是可序列化的,则它的子类也自动地可以序列化(接口的本意就是如此)。

import java.io.*;public class Box implements Serializable {private int width;private int height;public void setWidth(int w) {width = w;}public void setHeight(int h) {height = h;}public static void main(String[] args) {Box myBox = new Box();myBox.setWidth(50);myBox.setHeight(20);try {FileOutputStream fs = new FileOutputStream("foo.ser");ObjectOutputStream os = new ObjectOutputStream(fs);os.writeObject(myBox);os.close();} catch (Exception ex) {ex.printStackTrace();}}
}

序列化时全有或全无的 

整个对象版图都必须正确地序列化,不外就得全部失败如果Duck对象不能序列化,Pond对象就不能被序列化。

14.6 使用瞬时变量

如果某实例变量不能或不应该被序列化,就把它标记为transient(瞬时)的

如果你需要序列化程序能够跳过某个实例变量,就把它标记成transient的变量

import java.io.*;public class Chat implements Serializable {transient  String currentID;String userName;//还有更多程序代码……
}

如果你有无法序列化的变量不能被存储,可以用transient这个关键词把它标记出来,序列化程序会把它跳过。

为什么有些变量不能被序列化?可能是设计者忘记实现Serializable。或者动态数据只可以在执行时求出而不能或不必存储。虽然Java函数库中大部分的类可以被序列化,你还是无法将网络联机之类的东西保存下来。它得要在执行期当场创建才有意义。一旦程序关闭之后,联机本身就不再有用,下次执行时需要重新创建出来。 

14.7 对象解序列化

解序列化(Deserialization):还原对象

将对象序列化整件事情的重点在于你可以在事后,在不同的Java虚拟机执行期(甚至不是同一个Java虚拟机),把对象恢复到存储时的状态。解序列化有点像是序列化的反向操作。

1.创建FileInputStream

FileInputStream fileStream = new FileInputStream("Mygame.ser");

2.创建ObjectInputStream

ObjectInputStream os = new ObjectInputStream(fileStream);

3.读取对象

Object one = os.readObject();
Object two = os.readObject();
Object three = os.readObject();

4.转换对象类型

GameCharacter elf = (GameCharacter) one;
GameCharacter troll = (GameCharacter) two;
GameCharacter magician = (GameCharacter) three;

5.关闭ObjectInputStream 

os.close();

解序列化的时候发生了什么事?

当对象被解序列化时,Java虚拟机会通过尝试在堆上创建新的对象,让它维持与被序列化时有相同的状态来恢复对象的原状。但这当然不包括transient的变量,它们不是null(对对象引用而言)不然就是使用primitive主数据类型的默认值。

1.对象从stream中读出来。

2.Java虚拟机通过存储的信息判断出对象的class类型。

3.Java虚拟机尝试寻找和加载对象的类。如果Java虚拟机找不到或无法加载该类,则Java虚拟机会抛出例外。

4.新的对象会被配置在堆上,但构造函数不会执行!很明显的,这样会把对象的状态抹去又变成全新的,而这不是我们想要的结果。我们需要的是对象回到存储时的状态。

5.如果对象在继承树上有个不可序列化的祖先类,则该不可序列化类以及在它之上的类的构造函数(就算是可序列化也一样)就会执行。一旦构造函数连锁启动之后将无法停止。也就是说,从第一个不可序列化的父类开始,全部都会重新初始状态。

6.对象的实例变量会被还原成序列化时点的状态值。transient变量会被赋值null的对象引用或primitive主数据类型的默认为0、false等值。

存储与恢复游戏人物

import java.io.*;public class GameSaverTest {public static void main(String[] args) {GameCharacter one = new GameCharacter(50, "Elf", new String[] {"bow", "sword", "dust"});GameCharacter two = new GameCharacter(200, "Troll", new String[] {"bare hands", "big ax"});GameCharacter three = new GameCharacter(120, "Magician", new String[] {"spell", "invisibility"});// 假设此处有改变人物状态的程序代码try {ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Game.ser"));os.writeObject(one);os.writeObject(two);os.writeObject(three);os.close();} catch (IOException ex) {ex.printStackTrace();}one = null;two = null;three = null;try {ObjectInputStream is = new ObjectInputStream(new FileInputStream("Game.ser"));GameCharacter oneRestore = (GameCharacter) is.readObject();GameCharacter twoRestore = (GameCharacter) is.readObject();GameCharacter threeRestore = (GameCharacter) is.readObject();System.out.println("One's type: " + oneRestore.getType());System.out.println("Two's type: " + twoRestore.getType());System.out.println("Three's type: " + threeRestore.getType());} catch (Exception ex) {ex.printStackTrace();}}
}
import java.io.*;public class GameCharacter implements Serializable {int power;String type;String[] weapons;public GameCharacter(int p, String t, String[] w) {power = p;type = t;weapons = w;}public int getPower() {return power;}public String getType() {return type;}public String getWeapons() {String weaponList = "";for (int i = 0; i < weapons.length; i++) {weaponList += weapons[i] + " ";}return  weaponList;}
}

14.8 写入文本文件

将字符串写入文本文件

通过序列化来存储对象是Java程序在来回执行间存储和恢复数据最简单的方式。但有时你还得把数据存储到单纯的文本文件中。假设你的Java程序必须把数据写到文本文件中以让其他可能是非Java的程序读取。例如你的servlet(在Web服务器上执行的Java程序)会读取用户在网页上输入的数据,并将它写入文本文件以让网站管理人能够用电子表格来分析数据。

写入文本数据(字符串)与写入对象是很类似的,你可以使用FileWrite来代替FileOutputStream(当然不会把它链接到ObjectOutputStream上)。

写序列化的对象:

ObjectOutputStream.writeObject(someObject);

写字符串:

fileWriter.write("My first String to save");
import java.io.*;public class WriteAFile {public static void main(String[] args) {try {FileWriter writer = new FileWriter("Foo.txt");writer.write("hello foo!");writer.close();} catch (IOException ex) {ex.printStackTrace();}}
}

14.9 java.io.File

File这个类代表磁盘上的文件,但并不是文件中的内容。啥?你可以把File对象想象成文件的路径,而不是文件本身。例如File并没有读写文件的方法。关于File有个很有用的功能就是它提供一种比使用字符串文件名来表示文件更安全的方式。举例来说,在构造函数中取用字符串文件名的类也可以用File对象来代替该参数,以便检查路径是否合法等,然后再把对象传给FileWriter或FileInputStream。

File对象代表磁盘上的文件或目录的路径名称,如:
/Users/Kathy/Data/GameFile.txt
但它并不能读取或代表文件中的数据。

你可以对File对象做的事情:

1.创建出代表现存盘文件的File对象。

File f = new File("MyCode.txt");

2.建立新的目录。

File dir = new File("Chapter7");
dir.mkdir();

3.列出目录下的内容。

if (dir.isDirectory()) {String[] dirContents = dir.list();for (int i = 0; i < dirContents.length; i++) {System.out.println(dirContents);}
}

4.取得文件或目录的绝对路径。

System.out.println(dir.getAbsolutePath());

5.删除文件或目录(成功会返回true)。

boolean isDeleted = f.delete();

缓冲区的奥妙之处

没有缓冲区,就好像逛超市没有推车一样。你只能一次拿一项东西结账。 

缓冲区的奥妙之处在于使用缓冲区比没有使用缓冲区的效率更好。你也可以直接使用FileWriter,调用它的write()来写文件,但它每次都会直接写下去。
你应该不会喜欢这种方式额外的成本,因为每趟磁盘操作都比内存操作要花费更多时间。通过BufferedWriter和FileWriter的链接,BufferedWriter可以暂存一堆数据,然后到满的时候再实际写入磁盘,这样就可以减少对磁盘操作的次数。
如果你想要强制缓冲区立即写入,只要调用writer.flush()这个方法就可以要求缓冲区马上把内容写下去

14.10 读取文本文件

读取文本文件

从文本文件读数据是很简单的,但是这次我们会使用File对象来表示文件,以FileReader来执行实际的读取,并用BufferedReader来让读取更有效率。

读取是以while循环来逐行进行,一直到readLine()的结果为null为止。这是最常见的读取数据方式(几乎非序列化对象都是这样的):以while循环(实际上应该称为while循环测试)来读取,读到没有东西可以读的时候停止(通过读取结果为null来判断)。

import java.io.*;public class ReadAFile {public static void main(String[] args) {try {File myFile = new File("MyTest.txt");FileReader fileReader = new FileReader(myFile);BufferedReader reader = new BufferedReader(fileReader);String line = null;while ((line = reader.readLine()) != null) {System.out.println(line);}reader.close();} catch (Exception ex) {ex.printStackTrace();}}
}

14.11 拆分字符串

用String的split()解析

要如何分开问题和答案?

当你读取文件时,问题和答案是合并在同一行,以“/”字符来分开的。

String的split()可以把字符串拆开。

split()可以将字符串拆开成String的数组。

Version ID:序列化的识别

版本控制很重要!
如果你将对象序列化,则必须要有该类才能还原和使用该对象。OK,这是废话。但若你同时又修改了类会发生什么事?假设你尝试要把Dog对象带回来,而某个非transient的变量却已经从double被改成String。这样会很严重地违反Java的类型安全性。其实不只是修改会伤害兼容性,想想下列的情况:

会损害解序列化的修改:
删除实例变量。
改变实例变量的类型。
将非瞬时的实例变量改为瞬时的。
改变类的继承层次。
将类从可序列化改成不可序列化。
将实例变量改成静态的。

通常不会有事的修改:

加入新的实例变量(还原时会使用默认值)A
在继承层次中加入新的类。
从继承层次中删除类。
不会影响解序列化程序设定变量值的存取层次修改。
将实例变量从瞬时改成非瞬时(会使用默认值)。

使用serialVersionUID

每当对象被序列化的同时,该对象(以及所有在其版图上的对象)都会被“盖”上一个类的版本识别ID。这个ID被称为serialVersionUID,它是根据类的结构信息计算出来的。在对象被解序列化时,如果在对象被序列化之后类有了不同的serialVersionUID,则还原操作会失败!但你还可以有控制权。

如果你认为类有可能会演化,就把版本识别ID放在类中。

当Java尝试要还原对象时,它会比对对象与Java虚拟机上的类的serialVersionUID。例如,如果Dog实例是以23这个ID来序列化的(实际的ID长得多),当Java虚拟机要还原Dog对象时,它会先比对Dog对象和Dog类的serialVersionUID。如果版本不相符,Java虚拟机就会在还原过程中抛出异常。

因此,解决方案就是把serialVersionUID放在class中,让类在演化的过程中还维持相同的ID。
这只会在你有很小心地维护类的变动时才办得到!也就是说你得要对带回旧对象的任何问题负起全责。

若想知道某个类的serialVersionUID,则可以使用JavaDevelopment Kit里面所带的serialver工具来查询。 

14.12 程序料理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/134243.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

App备案-iOS云管理式证书 Distribution Managed 公钥及证书SHA-1指纹的获取方法

​ 根据近日工业和信息化部发布的《工业和信息化部关于开展移动互联网应用程序备案工作的通知》&#xff0c;相信不少要进行IOS平台App备案的朋友遇到了一个问题&#xff0c;就是apple不提供云管理式证书的下载&#xff0c;也就无法获取公钥及证书SHA-1指纹。 ​ 已经上架的应用…

aosp定制android系统

目录 AOSP 准备工作(配置) 确定机型和版本 初始化 git安装 curl安装 同步源码 环境变量 创建aosp目录 指定同步版本 解下来安装编译需要的依赖 编译aosp源码 刷入系统 AOSP 全称 Android Open Source Project 是指Android开源项目&#xff0c;它是由Google主导的…

【有源码】基于uniapp的农场管理小程序springboot基于微信小程序的农场检测系统(源码 调试 lw 开题报告ppt)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人七年开发经验&#xff0c;擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等&#xff0c;大家有这一块的问题可以一起交流&#xff01; &#x1f495;&…

算法:分治法-力扣题最大子数组和

文章目录 概念应用步骤实现过程-快速排序为例具体实现步骤&#xff1a;代码实现&#xff1a; 力扣-2586统计范围内的元音字符题解 概念 分治法是一种算法思想&#xff0c;其核心思想是将一个大问题分割成若干个小问题来解决。通过对小问题的分别计算&#xff0c;最终得到大问题…

SEO是什么?独立站如何进行SEO优化

创建一个独立网站并不是难事&#xff0c;但要做好独立网站并进行SEO优化以增加自然流量可能是一个不小的挑战。今天&#xff0c;我们将分享一些关于独立网站SEO优化的技巧&#xff0c;并详细探讨如何提升流量。 在本文中&#xff0c;我们将主要关注谷歌SEO&#xff0c;但请不要…

2000-2022年上市公司专利申请、创新绩效数据

2000-2022年上市公司专利申请、创新绩效数据 1、时间&#xff1a;2000-2022年 2、指标&#xff1a;年份、股票代码、股票简称、行业名称、行业代码、省份、城市、区县、行政区划代码、城市代码、区县代码、首次上市年份、上市状态、专利申请总量、发明专利申请总量、实用新型…

Linux--vim

文章目录 Vim的介绍Vim的几种模式命令模式下的基本操作批量化注释Vim的简单配置使用插件 Vim的介绍 Vim是一个强大的文本编辑器&#xff0c;是从vi编辑器发展而来的&#xff0c;在vi编辑器的基础上进行了改进和拓展&#xff0c;具有强大的特性和功能。 Vim是一个自由开源软件&…

技术分享 | app自动化测试(Android)--显式等待机制

WebDriverWait类解析 WebDriverWait 用法代码 Python 版本 WebDriverWait( driver,timeout,poll_frequency0.5,ignored_exceptionsNone) 参数解析&#xff1a; driver&#xff1a;WebDriver 实例对象 timeout: 最长等待时间&#xff0c;单位秒 poll_frequency: 检测的间…

MySQL 8.0 Clone Plugin 详解

文章目录 前言1. 克隆插件安装2. 克隆插件的使用2.1 本地克隆2.2 远程克隆 3. 克隆任务监控4. 克隆插件实现4.1 Init 阶段4.2 File Copy4.3 Page Copy4.4 Redo Copy4.5 Done 5. 克隆插件的限制6. 克隆插件与 Xtrabackup 的异同7. 克隆插件相关参数后记 前言 克隆插件&#xff…

Go uuid库介绍

简介&#xff1a; 在现代软件开发中&#xff0c;全球唯一标识符&#xff08;UUID&#xff09;在许多场景中发挥着重要的作用。UUID是一种128位的唯一标识符&#xff0c;它能够保证在全球范围内不重复。在Go语言中&#xff0c;我们可以使用第三方库github.com/google/uuid来方便…

python 之 集合的相关知识

文章目录 1. 创建集合使用花括号 {}使用 set() 函数 2. 集合的特点3. 集合操作添加元素删除元素 4. 集合运算5. 不可变集合总结 在 Python 中&#xff0c;集合&#xff08;Set&#xff09;是一种无序且不重复的数据集合。它是由一组唯一元素组成的。下面是关于集合的一些基本知…

【云原生】使用nginx反向代理后台多服务器

背景 随着业务发展&#xff0c; 用户访问量激增&#xff0c;单台服务器已经无法满足现有的访问压力&#xff0c;研究后需要将后台服务从原来的单台升级为多台服务器&#xff0c;那么原来的访问方式无法满足&#xff0c;所以引入nginx来代理多台服务器&#xff0c;统一请求入口…

Leetcode-234 回文链表

我的解法&#xff1a;使用栈&#xff0c;定义了len略微复杂&#xff0c;拿链表的后半部分和前半部分比较即可&#xff0c;没必要全部比较 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* List…

369-HI-R-M-0-0-0-E 数字化转型如何改变DCS和SCADA

369-HI-R-M-0-0-0-E 数字化转型如何改变DCS和SCADA 高瞻远瞩的过程制造商正在投资数字化转型&#xff0c;而DCS和SCADA最终将成为这些努力的一部分。因此&#xff0c;它们与所有其他流程制造技术一起发展。DCS和SCADA系统的变化符合自动化金字塔正在进行的转变&#xff0c;它也…

Linux驱动开发——USB设备驱动

目录 一、 USB 协议简介 二、 Linux USB 驱动 三、 USB 设备驱动实例 一、 USB 协议简介 USB(Universal Serial Bus&#xff0c;通用串行总线)正如它的名字一样&#xff0c;是用来连接PC外设的一种通用串行总线&#xff0c;即插即用和易扩展是它最大的特点。所谓即插即用&am…

GPT-4V:AI在医疗领域的应用

OpenAI最新发布的GPT-4V模型为ChatGPT增添了语音和图像功能&#xff0c;为用户提供了更多在日常生活中使用ChatGPT的方式。这次更新将为用户带来更加便捷、直观的交互体验&#xff0c;用户可以直接通过拍照上传图片&#xff0c;并提出相关问题。OpenAI的最终目标是构建一个安全…

MYSQL多表联查on和where的区别

目录 一、背景 二、探究 2.1、统计每个班级中女生的数量 错误的写法 查询结果 正确的写法 查询结果 2.2、只统计"一班"的学生数量 错误的写法 查询结果 正确的写法 查询结果 三、总结 一、背景 在一次对数据进行统计的时候&#xff0c;需要对两张表进行…

【vite】vite.defineConfig is not a function/npm无法安装第三方包问题

当使用vite命令 npm init vite-app 项目名称时配置 import vue from vitejs/plugin-vueexport default defineConfig({plugins: [vue()] })会报错vite.defineConfig is not a function 还有就是npm下载的时候也会报错 原因vite插件vitejs/plugin-vue和vite版本问题 解决 调…

Intel oneAPI笔记(3)--jupyter官方文档(SYCL Program Structure)学习笔记

前言 本文是对jupyterlab中oneAPI_Essentials/02_SYCL_Program_Structure文档的学习记录&#xff0c;包含对Device Selector、Data Parallel Kernel、Host Accessor、Buffer Destruction、的介绍&#xff0c;最后还有一个小关于向量&#xff08;Vector&#xff09;加法的实例 …

zookeeper:启动原理

主类&#xff1a; QuorumPeerMain, 其中调用了main对象的initializeAndRun方法&#xff0c; 首先定义了QuorumPeerConfig对象&#xff0c;然后调用了parse方法&#xff0c;parse方法代码如下&#xff1a; 其中调用的parseProperties方法的代码如下&#xff1a; 可以看到&am…