第十四章 序列化的文件的输入/输出
1. 储存对象
我们在很多地方都需要用到储存这个功能。储存状态的选择有很多种,这需要看你如何使用储存下来的状态而决定。在这里我们只讨论以下两种选项。
-
如果只有自己写的Java程序会用到的这些数据:
使用序列化(serialization)。将被序列化的对象写到文件中,这样可以让你的程序去文件中读取序列化的对象并把它们展开回原本的状态。 -
如果数据要被其他程序引用
写一个纯文本文件。用其他程序可以解析的特殊字符写到文件中。
储存状态
假设我们要储存三种游戏角色的对象,那我们便可以有如下的两种方法。
-
序列化
我们可以将3个对象以序列化的方法写入文件,此时序列化的代码是难以阅读的,但是它比纯文本更容易让程序恢复,也比较安全,因为一般人难以去修改数据。 -
纯文本文件
创建文件,写入3行文字,每个人物一行,以逗号来分割属性。
下面是将对象序列化(储存)的方法步骤
//创建出FileOutputStream
FileOutputStream fileStream = new FileOutputStream("MyGame.ser"); // 如果该文件不存在,则会自动创建
// 创建ObjectOutputStream
ObjectOutputStream os = new ObjectOutputStream(fileStream); //它能让你写入对象,但无法直接连接文件,所以需要参数指引
// 写入对象
os.wtiteObject(characterOne); // 将变量所引用的对象序列化并写入MyGame.ser这个文件
os.writeObject(characterTwo);
os.writeObject(characterThree);
// 关闭ObjectOutputStream
os.close(); // 关闭所关联的输出串流
串流
数据在串流(stream)中移动,Java的输入/输出API带有连接类型的串流,它代表来源与目的地之间(文件与网络客户端)的连接,连接串流将串流与其他串流连接起来。串流必须要连接到某处才能算是个串流。
一般来说串流要两两连接才能有意义——一个表示连接,另一个则为要调用的方法。因为连接的串流通常都是很低层的,虽然有些串流有别的方法,但是考虑到面向对象,我们一般只是用其中一种方式。每个类做好一件事,通过不同串流的组合来实现最大的适应性。
2. 对象序列化
序列化程序会将对象版图上的所有东西存储起来。被对象的实例变量所引用的所有对象都会被序列化。
如果要让类能够被序列化,就实现Serializable。Serializable接口又被称为marker或tag类的标记用接口,因为此接口并没有任何方法需要实现。它唯一的目的就是声明有实现它的类是可以被序列化的。也就是说,此类型的对象可以通过序列化的机制存储。如果某类是可序列化的,则它的子类也自动地可以序列化(接口的本意即使如此)。
import java.io.*; // 必须要引用其
public class Box implements Serializable { // 接上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(30);
try {
FileOutputStream fs = new FileOutputStream("foo.ser"); //没有foo.ser文件则会创建出来
ObjectOutputStream os = new ObjectOutputStream(fs);
os.writeObject(mybox);
os.close();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
如果某实例变量不能或不应该被序列化,就把它标记为transient(瞬时)的。
import java.net.*;
class Chat implements Serializable {
transient String currentID; // 这个变量将不会被序列化
String userName; // 这个变量会被序列化
...
}
使用transient可以让序列化程序时跳过变量(不序列化,引用的实例变量以null返回)。在Java中有部分无法被序列化的变量,可以使用该方法跳过它们。或者将其用于你不想序列化的变量上。
PS: 小总结: 以特定的方式对类实例的瞬时状态进行编码保存的一种操作。从此定义可以看出,序列化作用的对象是类的实例。对实例进行序列化,就是保存实例当前在内存中的状态.包括实例的每一个属性的值和引用等。
既然后序列化,便会有反序列化。反序列化的作用便是将序列化后的编码解码成类实例的瞬时状态。申请等同的内存保存该实例。
从上述定义可以发现,序列化就是为了保存java的类对象的状态的。保存这个状态的作用主要用于不同jvm之间进行类实例间的共享。在ORMaping中的缓存机制,进行缓存同步时,便是常见的java序列化的应用之一。在进行远程方法调用,远程过程调用时,采用序列化对象的传输也是一种应用。当你想从一个jvm中调用另一个jvm的对象时,你就可以考虑使用序列化了。
简而言之:序列化的作用就是为了不同jvm之间共享实例对象的一种解决方案。由java提供此机制,效率之高是其他解决方案无法比拟的。
3. 解序列化(Deserialization)
解序列化能把对象恢复到储存时的状态,就像序列化的反向操作。
// 创建FileInputStream
FileInputStream fileStream = new FileInputStream("MyGame.ser"); // 使用try,如果文件不存在则抛出异常
// 创建ObjectInputStream
ObjectInputStream os = new ObjectInputStream(fileStream); // 其知道如何读取对象,但是要通过连接的Stream提供文件存取
// 读取对象
Object one = os.readObject(); // 每次调用readOBject()都会从stream中读出下一个对象,顺序和存入相同,次数超过会抛出异常
Object two = os.readObject();
Object three = os.readObject();
// 转换对象类型
GameCharacter elf = (GameCharacter) one; // 返回的是Object类型的值,因此要转换类型
GameCharacter throll = (GameCharacter) two;
GameCharacter magician = (GameCharacter) three;
//关闭ObjectInputStream
os.close(); // FileInputStream会跟随自动关掉
Note
: 解序列化时对象会被重新放会在堆上,但是构造函数是不会执行(执行了的话数据之类的都没了)。但是如果它继承了某个没有不可序列化的祖先类,则从这个不可序列化的祖先类开始往上所有的构造函数都会执行,也就是说,从第一个不可序列化的父类开始,全部都会重新初始化状态。
对象序列化要点
- 你可以通过序列化来储存对象的状态
- 使用ObjectOutputStream来序列化对象(Java.io)
- Stream时连接串流或是链接用的串流
- 连接串流用来表示源或目的地、文件、网络套接字连接
- 链接用串流用来链接链接串流
- 用FileOutputStream链接ObjectOutputStream来将对象序列化到文件上
- 调用ObjectOutputStream的writeObject(theObject)来将对象序列化。不需调用FileOutputStream的方法
- 对象必须实现序列化这个接口才能被序列化。如果父类实现序列化,则子类也就自动地有实现,而不管是否有明确的声明
- 当对象被序列化时, 整个对象版图都会被序列化。这代表它的实例变量所引用的对象也会被序列化
- 如果有不能序列化的对象,执行期间就会抛出异常
- 除非该实例变量被标记为transient。否则,该变量在还原的时候会被赋予null或primitive主数据类型的默认值
- 读取对象的顺序必须与写入的顺序相同
- readObject()的返回类型是Object,因此解序列化回来的对象还需要转换成原来的类型
- 静态变量不会被序列化,因为所有对象都是共享同一份静态变量值
4. 文本文件存储读取
写入文本数据(字符串)与写入对象是类似的,可以使用FileWrite来代替FIleOutputStream。
import java.io.*; // 一定要加载这个包
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();
}
}
}
java.io.File.class
Flie也是可以一种存储文件所用的类。File这个类代表磁盘上的文件,但并不是文件中的内容。你可以把File对象想象成文件的路径,而不是文件本身。例如FIle并没有读写文件的方法。关于FIle有个非常实用的功能就是它提供一种比使用字符串文件名来表示文件更安全的方式。举例来说,在构造函数中取用字符串文件名的类也可以用File对象来代替该参数,以便检查路径是否合法等,然后再把对象传给FileWriter或者FileInputStream。下面是File对象的一些方法:
// 创建出代表现存盘文件的File对象
File F = new File("MyCodde.txt");
// 建立新的目录
File dir = new File("Chapter 7");
dir.mkdir();
// 列出目录下的内容
if (dir.isDirectory()) {
String[] dirContents = dir.list();
for(int i = 0; i < dirContents.length; i++) {
System.out.println(dirContents[i]);
}
}
// 取得文件或目录的绝对路径
System.out.println(dir.getAbsolutePath());
// 删除文件或目录(成功会返回true)
boolean isDeleted = f.delete();
缓冲区
缓冲区就像在超市的购物车一样,你可以把货物一件一件放进去,最后一次性结账。它的奥妙之处在于使用缓冲区比没有使用缓冲区的效率更好。相比使用FIleWriter的write()方法来写入文件,它能节省磁盘操作的时间。通过BufferedWriter和FileWriter的连接,BufferedWriter可以暂存一堆数据,然后到满的时候再实际写入磁盘,这样就可以减少对磁盘操作的次数。
(PS:如果你想要强制缓冲区立即写入,只需要调用**Writer.flush()**这个方法就可以要求缓冲区马上把内容写下去)
BufferedWriter writer = new BufferedWriter(new FileWriter(aFile));
picture
读取文本文件
读取是以while循环来逐行进行,一直到readLine()的结果为null为止。这是最常见的读取数据方式(几乎非序列化对象都是这样的):以while循环(实际上应该称为while循环测试)来读取,读到没有东西可以读的时候停止(通过读取结果为null来判断)。
import java.io.*;
class ReadAFile {
public static void main(String[] args) {
try {
File myFilfe = new File("MyText.txt");
FileReader fileReader = new FileReader(myFile); // FileReader是字符连接到文本文件的串流
BufferedReader reader = new BufferedReader(fileReader); // 将FileReader连接到BufferedReader以获取更高的效率
String line = null; // 用String变量来承接所读取的结果
while((line = reader.readLine()) != null) {
System.out.println(line); // 读一行列一行,直到没有东西可读为止
}
reader.close();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
要点
- 用FIleWriter这个连接串流来写入文本文件
- 将FileWriter链接到BufferedWriter可以提升效率
- FIle对象代表文件的路径而不是文件本身
- 你可以用File对象来创建、浏览和删除项目
- 用到String文件名的串流大部分都可以用File对象来代替String
- 用FileReader来读取文本文件
- 将FIleReader链接到BufferedReader可以提升效率
- 通常我们会使用特殊的字符来分隔文本数据中的不同元素
- 使用split() 方法可以把String拆开,其中的分隔字符不会被当作数据来看待