Java I/O (2) - 对象的输入输出与序列化
先说概念:
一、相关概念
序列化:把内存中的java对象转换成与平台无关的二进制字节序列,以便永久保存在磁盘上或通过网络进行传输。序列化是Java提供的一种将对象写入到输出流、并在之后将其读回的机制。
Java提供的对对象进行读写的流对象(即承载对象的媒介)为ObjectOutputStream 和 ObjectInputStream ,它们的作用就是把对象转换为字节序列,并承载这些序列,并提供了writeObject() 和 readObject() 方法对这些字节序列进行读写。
那么,如何实现对象的序列化与反序列化呢?
二、序列化、反序列化的方法
方法(一)-- Serializable接口
2.1 使用Serializable接口的默认序列化机制
1、实现Serializable接口
如果某个类对象要被序列化,该类必须实现Serializable接口,只接口内无具体方法,所以只需要声明实现该接口,而无需实现具体方法。
public class Student implements Serializable { …… }
2、通过ObjectOutputStream 类对象的writeObject(obj)方法,将obj参数指定的对象进行序列化,把得到的字节序列写到目标输出流中。
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat")); Student s = new Student(); out.writeObject(s); out.close();
3、通过ObjInputStream类对象的readObject()方法从源输入流中读取字节序列,再反序列化为对象,并将其返回。
//反序列化得到对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat")); Student s1 = (Student) in.readObject(); in.close();
以上是简单的对象序列化,使用默认序列化机制,会将对象的全部域都进行序列化,同时对该对象引用的其他对象也进行序列化,同样的,对这些引用对象进行序列化的时候如果有另外的引用,这些引用也会被序列化,以此类推。如果一个对象包含的成员变量是容器对象,容器的元素还是容器,那么序列化过程会变得十分繁琐。另外,有时候对象的一些数据域只在本地有意义,对这样的数据进行序列化后存储或传输没有意义,甚至有时会在反序列化后使用对象时引起崩溃。所以,我们需要指定一些字段不需要序列化。
2.2 自定义序列化
序列化的实际应用中通常需要忽略某些数据域或简化序列化过程,此时就需要自定义序列化的过程,自定义序列化的方法有如下几种:1、使用transient关键字;2、使用writeObject()和readObject()方法 3、使用External接口实现序列化。
1、使用transient关键字
在对未来会进行序列化的类进行定义时,使用transient关键字声明无需序列化的字段。默认序列化机制会忽略被transient修饰的字段。
2、使用writeObject()与readObject()
在可序列化的类中添加私有的writeObject和readObject方法,定义这个两个方法后,不再使用默认的序列化机制,而是执行这两个方法中的内容。在这个方法中可以向默认的读写行为中添加验证或任何其他行为。
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ in.defaultReadObject();//调用默认序列化机制 //添加逻辑 } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject();//调用默认序列化机制 //添加逻辑 }
使用这种方法可以序列化某些不可被序列化的域。有类似这样的情形,某可序列化的类中一个域是某不可序列化的类的对象,但是又需要反序列化的时候获取这个对象。此时可以使用这里提到的方法进行序列化。
step1:将不可序列化的类的对象声明为transient,以避免NotSerializableException。示例:
(student可序列化,teacher不可)
public class Student implements Serializable { private String name; private int age; private transient Teacher teacher; ………… }
step2:writeObject方法中调用defaultWriteObject方法写出可序列化的域,然后使用标准的DataOutput调用写出不可序列化的域中的数据。defaultWriteObject是ObjectOutputStream类的一个特殊方法,只能在可序列化类的writeObject方法中调用。注意该方法为private类型。示例:
private void writeObject(ObjectOutputStream out) throws IOException { //对可序列化的域进行序列化 out.defaultWriteObject(); //不可序列化的域对该域内基本数据类型分别进行序列化 out.writeInt(teacher.getCourseNo()); out.writeUTF(teacher.getName()); }
step3:readObject方法中反过来执行上述过程,注意该方法也为private类型。示例:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ //对可序列化的域进行反序列化 in.defaultReadObject(); //不可序列化的域通过对该域内基本数据类型分别进行反序列化,然后重新创建该域 int courseNo = in.readInt(); String name = in.readUTF(); teacher = new Teacher(courseNo, name); }
完整示例代码:
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Student implements Serializable { private String name; private int age; private transient Teacher teacher; public Student(){} public void setName(String n, int a){ this.name = n; this.age = a; } public String getName(){ return this.name; } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ //对可序列化的域进行反序列化 in.defaultReadObject(); //不可序列化的域通过对该域内基本数据类型分别进行反序列化,然后重新创建该域 int courseNo = in.readInt(); String name = in.readUTF(); teacher = new Teacher(courseNo, name); } private void writeObject(ObjectOutputStream out) throws IOException { //对可序列化的域进行序列化 out.defaultWriteObject(); //不可序列化的域对该域内基本数据类型分别进行序列化 out.writeInt(teacher.getCourseNo()); out.writeUTF(teacher.getName()); } }
public class Teacher { private int courseNo; private String name; public int getCourseNo() { return courseNo; } public String getName() { return name; } public Teacher(int courseNo, String name){ this.courseNo = courseNo; this.name = name; } }
注意,
方法(二)-- External接口
3、使用Externalizable接口
无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。除了让序列化机制来保存和恢复对象,类还可以定义它自己的机制。JDK中提供了另一个序列化接口--Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。声明实现Externalizable接口的类必须重写readExternal()和writeExternal()方法。示例:
import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; public class Student2 implements Externalizable { public String name; public int age; public Teacher teacher; public Student2(){} @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeUTF(name); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = in.readUTF(); age = in.readInt(); } }
Externalizable继承于Serializable,跟readObject方法不同的是,使用该接口时,序列化的细节需要程序员自行完成,writeExternal和readExternal方法需要对包括超类数据在内的整个对象的存储和恢复负全责。
在写出对象时,序列化机制在输出流中仅仅是记录该对象所属的类。在读入可外部化的类时,对象输入流会调用类的无参构造函数创建一个对象,然后调用readExternal方法。所以实现Externalizable的类必须提供一个无参构造函数,并且权限为public。
注意:readObject和writeObject是私有的,并且只能被序列化机制调用;readExternal 和 writeExternal是公共的,readExternal还潜在的允许修改现有对象的状态。
三、总结
1、java序列化就是为了存储或传输对象的某个瞬时状态。
2、java中实现对象序列化的方式有两种:
方法一:
可序列化类实现Serializable接口,该接口默认情况下没有需要实现的方法。
如果有不可序列化的字段,如果该字段在不需要反序列化,则使用transient关键字声明该字段即可;如果该字段在反序列化后还有需要,则通过在可序列化类中定义writeObject和readObjcet方法,将不可序列化的字段中的数据分别进行序列化后,在反序列化时读取数据并重新创建对象。
方法二:
可序列化类实现Extrernal接口,该接口必须实现readExternal和weiteExternal方法。并在该方法内对每个需要进行序列化的字段挨个进行序列化,反序列化时再挨个反序列化恢复。