一、序列化与反序列化
Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。
如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。
简单来说:
-
序列化:将数据结构或对象转换成二进制字节流的过程
-
反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程
类必须实现Serializable或Externalizable接口以便序列化或反序列化。Serializable接口是一个标记接口(空接口)。如果我们想要一个Person类的对象被序列化,我们需要声明Person类如下:
public class Person implements Serializable {
}
Java负责处理从/向流读取/写入Serializable对象的细节。我们只需要将对象写入/读取流到流类中的一个方法。实现Externalizable接口使我们能够更好地控制从流中读取和写入对象。它继承Serializable接口。它声明如下:
public interface Externalizable extends Serializable {
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
void writeExternal(ObjectOutput out) throws IOException;
}
当我们从流中读取一个对象时,Java调用readExternal()方法。当我们向一个流写一个对象时,它调用writeExternal()方法。我们必须编写逻辑来分别读取和写入readExternal()和writeExternal()方法中的对象的字段。实现Externalizable接口的类如下所示:
public class Person implements Externalizable {
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// Write the logic to read the Person object fields from the stream
}
public void writeExternal(ObjectOutput out) throws IOException {
// Write the logic to write Person object fields to the stream
}
}
二、ObjectInputStream 和 ObjectOutputStream
ObjectInputStream 和 ObjectOutputStream 是高层次的数据流,它们包含反序列化和序列化对象的方法,类必须实现Serializable或Externalizable接口才能使用 ObjectInputStream 和 ObjectOutputStream。
1、ObjectInputStream
构造函数与常用方法:
protected ObjectInputStream();//为完全重新实现 ObjectInputStream 的子类提供一种方式,让它不必分配仅由 ObjectInputStream 的实现使用的私有数据。
public ObjectInputStream(InputStream in);//创建从指定 InputStream 读取的 ObjectInputStream。从流读取序列化头部并予以验证。在对应的 ObjectOutputStream 写入并刷新头部之前,此构造方法将阻塞。
public final Object readObject();//从 ObjectInputStream 读取对象。对象的类、类的签名和类及所有其超类型的非瞬态和非静态字段的值都将被读取。
public int read();//读取一个字节的数据。
public boolean readBoolean();//读取布尔值。
public int readInt();//读取一个32位int。
public void close();//关闭输入流。
2、ObjectIOutputStream
构造函数与常用方法:
protected ObjectOutputStream();//为完全重新实现 ObjectOutputStream 的子类提供一种方法,让它不必分配仅由 ObjectOutputStream 的实现使用的私有数据。
public ObjectOutputStream(OutputStream out);//创建写入指定 OutputStream 的 ObjectOutputStream。此构造方法将序列化流部分写入底层流;调用者可以通过立即刷新流,确保在读取头部时,用于接收 ObjectInputStreams 构造方法不会阻塞。
public final void writeObject(Object obj);//将指定的对象写入 ObjectOutputStream。对象的类、类的签名,以及类及其所有超类型的非瞬态和非静态字段的值都将被写入。
void flush();//刷新流。
void close();//关闭流。
void write(byte[] buf);//写入一个字节数组。
void write(byte[] buf, int off, int len);//写入一个子字节数组。
void write(int val);//写一个字节。
void writeBoolean(boolean val);//写一个布尔值。
void writeByte(int val);//写入一个8位字节。
void writeBytes(String str);//写一个字符串作为字节序列。
void writeFloat(float val);//写一个32位浮点数。
void writeInt(int val);//写一个32位int。
void writeLong(long val);//写一个64位长
3、序列化与反序列化示例
class Person implements Serializable{
private String name;
private int age;
public Person(String name, int age){
System.out.println("实例化对象");
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class TestDemo8 {
public static void main(String[] args) {
//序列化Person对象
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
Person p = new Person("tulun", 18);
oos.writeObject(p);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//反序列出Person对象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("a.txt"));
Person p = (Person)ois.readObject();
System.out.println(p.getName()+":: "+p.getAge());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:序列化号 serialVersionUID 属于版本控制的作用。序列化的时候 serialVersionUID 也会被写入二级制序列,当反序列化时会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException
异常。强烈推荐每个序列化类都手动指定其 serialVersionUID
,如果不手动指定,那么编译器会动态生成默认的序列化号。制定序列化号的方法如下:
public class RpcRequest implements Serializable {
private static final long serialVersionUID = 1905122041950251207L;
//......
}
三、 transient关键字与自定义序列化
transient关键字可以使被修饰的属性不会被Java默认的方式序列化和反序列化,如果要对transient关键字修饰的属性进行序列化和反序列化,需要在类中实现writeObject方法和readObject方法。
Java并不强求用户非要使用默认的序列化方式,用户也可以按照自己的喜好自己指定自己想要的序列化方式。手动指定序列化方式的规则是:进行序列化、反序列化时,虚拟机会首先试图调用对象里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,那么默认调用的是ObjectOutputStream的defaultWriteObject以及ObjectInputStream的defaultReadObject方法。换言之,利用自定义的writeObject方法和readObject方法,用户可以自己控制序列化和反序列化的过程。
在类中添加writeObject和readObject方法可以实现自定义序列化,实现模板如下:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// 先调用默认的序列化方式,将没有被transient修饰的属性进行序列化
s.defaultWriteObject();
// 自定义序列化,其实就是调用 输出流对象 s 将被transient修饰的属性输出
……
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 先调用默认的反序列化方式,将没有被transient修饰的属性进行反序列化
s.defaultReadObject();
// 自定义反序列化,其实就是调用 输入流对象 s 将被transient修饰的属性读取进来
……
}
writeObject和readObject的实现顺序要相互对应,比如,第一步都是默认的方法,第二步都处理name字段,第三部都处理age字段等。