第14章:序列化与文件的输入输出
对象可以被序列化,也可以展开。对象有状态和行为两种属性,行为存在于类中,而状态存在于个别的对象中。本章将讨论以下两种选项:
1.如果只有自己写的Java程序会用到这些数据。用序列化(Serialization),将被序列化的对象写到文件中。然后就可以让你的程序去文件中读取序列化的对象,并把它们展开回到活生生的状态。
2.如果数据需要被其它程序引用。写一个纯文本文件,用其它程序可以解析的特殊字符写到文件中。
如何把序列化对象写入文件?
public class TestOutputStream {
public static void main(String[] args) {
try {
// 如果文件不存在,就自动创建该文件
FileOutputStream fileStream = new FileOutputStream("mygame.ser");
// 创建存取文件的 os 对象
ObjectOutputStream os = new ObjectOutputStream(fileStream);
// 把序列化对象写入文件
os.writeObject(new Dog()); // Dog必须是可序列化的类
os.writeObject(new Dog());
// 关闭所关联的输出串流
os.close();
} catch (Exception e) {
System.out.println(e);
}
}
}
什么是串流?
将串流( stream )连接起来代表来源与目的地(文件或网络端口)的连接。串流必须要连接到某处才能算是个串流。Java的输入输出API带有连接类型的串流,它代表来源与目的地之间的连接,连接串流即把串流与其它串流连接起来。
当对象被序列化时,被该对象引用的实例变量也会被序列化。且所有被引用的对象也会被序列化。最重要的是,这些操作都是自动完成的。
如果要让类能够序列化,就要实现Serializable
Serializable接口,又被称为marker或tab类的标记接口,因为此接口并没有任何方法需要被实现。它唯一的目的就是声明有实现它的类是可以被序列化的。也就是说,此类型的对象可以通过序列化的机制来存储。如果某个类是可以序列化的,则它的子类也自动地可以序列化。
// Serializable没有方法需要实现,它只是用来告诉Java虚拟机这个类可以被序列化
public class Box implements Serializable {
public Box() {}
}
注意:序列化是全有或全无的,即对象序列化时不存在“一部分序列化成功、另一部分序列化失败”的情况,如果对象有一部分序列化失败,则整个序列化过程就是失败的。只有可序列化的对象才能被写入到串流中。
那么在一个可序列化的类中,如何指定部分实例变量不执行序列化呢?
如果希望某个实例变量不能或不应该被序列化,就把它标记为 transient(瞬时)的,即可。
// Serializable没有方法需要实现,它只是用来告诉Java虚拟机这个类可以被序列化
public class Box implements Serializable {
public Box() {}
// 该id变量,就不会被序列化
transient String id;
String username;
}
如何从文件中读取序列化对象,并将其还原?
把对象恢复到存储时的状态。解序列化,可看成是序列化的反向操作。
public class TestInputStream {
public static void main(String[] args) {
try {
// 打开文件流,如果文件不存在就会报错
FileInputStream fileStream = new FileInputStream("mygame.ser");
// 创建 输入流
ObjectInputStream os = new ObjectInputStream(fileStream);
// 读取 序列化对象
Object one = os.readObject();
Object two = os.readObject();
// 类型转换,还原对象类型
Dog d1 = (Dog)one;
Dog d2 = (Dog)two;
// 关注 输入流
os.close();
} catch (Exception e) {
System.out.println(e);
}
}
}
序列化(存储)和解序列化(恢复)的过程,到底到生了什么事?
你可以通过序列化来存储对象的状态,使用ObjectOutputStream来序列化对象。Stream是连接串流或是链接用的串流,连接串流用来表示源或目的地、文件、网络套接字连接。链接用串流用来链接连接串流。使用FileOutputStream将对象序列化到文件上。静态变量不会被序列化,因为所有对象都是共享同一份静态变量值。
对象必须实现Serializable 这个接口,才能被序列化。如果父类实现了它,则子类就自动地有实现。
解序列化时,所有的类都必须能让Java虚拟机找到。读取对象的顺序必须与写入时的顺序一致。
如何把字符串写入文件文件?
try {
// FileWriter
FileWriter writer = new FileWriter("foo.txt");
writer.write("hello foo!");
writer.close();
} catch (IOException e) {
System.out.print(e);
}
什么是缓冲区?为什么使用缓冲区会提升数据读写的效率?
没有缓冲区,就好像逛超市没有推车一样,你只能一次拿一项商品去结账。缓冲区能让你暂时地摆一堆东西,直到装满为止。用了缓冲区,就可以省下好几趟的来回。
缓冲区的奥妙之处在于,使用缓冲区比没有使用缓冲区的效率更好。通过 BufferedWriter 和 FileWriter 的链接,BufferedWriter 可以暂存一堆数据,等到满的时候再实际写入磁盘,这样就可以减少对磁盘的操作次数。如果想要强制缓冲区立即写入,只要调用 writer.flush() 这个方法即可。
如何从文本文件中读取数据?
File对象表示文件,FileReader用于执行实际的数据读取,BufferedReader让读取更有效率。读取数据,使用 while 循环来逐行读取,直到 readLine() 的结果为 null 为止。这是最常见的数据读取方式(几乎所有的非序列化对象都是这样的)。
什么是 serialVersionUID?为什么要使用 serialVersionUID?
每当对象被序列化的同时,该对象(以及所有在其版图上的对象)都会被“盖”上一个类的版本识别ID,这个ID就被称为 serialVersionUID ,它是根据类的结构信息计算出来的。在对象被解序列化时,如果在对象被序列化之后类有了不同的 serialVersionUID,则解序列化会失败。虽然会失败,但你还可以有控制权。
如果你认为类有可能会深化,就把版本识别ID(serialVersionUID)放在类中。当Java尝试解序列化还原对象时,它会对比对象与Java虚拟机上的类的serialVersionUID 是否相同。如果相同,则还原成功;否则,还原将失败,Java虚拟机就会抛出异常。因此,把 serialVersionUID 放在类中,让类在演化过程中保持 serialVersionUID 不变。
public class Dog {
// 类的版本识别ID
private static final long serialVersionUID = -54662325652236L;
}
若想知道某个类的 serialVersionUID 是多少?则可以使用 Java Development Kit 里的 serialver 工具进行查询。
serialver Dog
Dog: static final long serialVersionUID = -54662325652236L;