其实现在很多资料和书籍讲解java中的序列化和反序列化的知识都比较少了,其主要原因是这个机制并没有XML和JSON那样高效,而且它的跨平台能力十分有限,所以下面的讲解主要是拓展知识用的,如果你不想了解完全可以不看。
一.基本原理
在java中对象的最简单的序列化的方式就是一个类实现Serializable这个接口,然后再调用ObjectOutputStream的writeObject将这个对象的二进制码写到一个文件或者网络流中去。最简单的反序列化的方式就是一个类实现Serializable这个接口,然后再调用ObjectInputStream的readObject将这个对象的二进制码从一个文件或者网络流中读取出来。java的序列化和发,序列化是一个对象在网络中得以传输的基础(当然,现在基本上不使用对象流在网络中传输对象了,因为这样传输对象的效率不高,而且安全性也不高,最重要的时这种方法的跨平台性不好,即我们用java序列化传输的对象只能通过java的反序列化进行获取,如果我们想用python或者perl进行反序列化就会失效了,所以现在一般是通过更加标准的格式JSON或者XML实现对象的在线传送。当然,通过序列化将对象写到一个文件中,然后再用反序列化将这个对象从文件中读取出来还是比较常用的)。
上面提到的那种方法是虚拟机全权帮助我们来序列化和反序列化一个对象,如果我们想定制我们自己的对象流来进行序列化和反序列化怎么办呢?java给我们提供了5个方法来帮助我们定制自己的序列化和反序列化的(如果你们需要在自己的类中添加这5个方法,那么方法必须和下面列出的写的一模一样才行,因为java内部是靠反射来调用这些方法的):
1. private void writeObject(java.io.ObjectOutputStream out)
throws IOException
2. private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
3. private void readObjectNoData()
throws ObjectStreamException;
4. ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
(一般来说ANY-ACCESS-MODIFIER = private)
5. ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
(一般来说ANY-ACCESS-MODIFIER = private)
上面5个方法中前3个比较常用是在序列化和反序列化中控制对象流的方法,而后两个方法不太常用,因为他们是在序列化和反序列化中篡改对象流的方法。下面我们就来一一介绍一下这5个方法:
首先,这5个方法都是写在需要序列化的对象所代表的类的内部的,比如我们有ServerLogger这个类(当然,为了序列化这个类的对象我们需要让这个类实现Serializable这个接口,ServerLogger这个类的定义见最后),那么如果我们想要控制序列化和反序列化中的对象流,我们就要将这5个方法全部都写在ServerLogger这个类中(这个5个方法分别是writeReplace、writeObject、readObject、readResolve和readObjectNoData)。
如果我们需要对一个对象进行序列化的话(比如将一个对象写入到一个文件中,正如上面提到的,将文件写入到网络流中现在已经不常用了),我们需要调用ObjectOutputStream对象的writeObject方法,如下面的例子所示:
ServerLogger sl = new ServerLogger(0, "debug", "description")
FileOutputStream fos = new FileOutputStream("logger");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(sl);
如果我们在ServerLogger这个类中写了writeReplace方法和writeObject方法(注意:writeObject方法在java内部是通过反射机制查找private方法从而实现对writeObject方法进行调用的,所以writeObject方法必须是private的),那么当上面最后一行的writeObject这个方法被调用的时候,它内部会去依次调用写在ServerLogger类中的writeReplace方法和writeObject方法,如果其中一个函数没有写在ServerLogger的话,java内部会调用对应的默认实现方法,这个方法就是java内部实现好的标准的序列化对象的方法。
所以,ObjectOutputStream类的writeObject方法的实现是:如果在需要序列化的类的内部写了writeReplace方法和writeObject方法,那么就先调用writeReplace方法,然后再根据writeReplace方法的返回值调用writeObject方法(即,writeReplace方法返回什么类型的对象,java就调用那个对象内部的writeObject方法,所以一般来说如果我们想让java调用ServerLogger中的writeObject方法的话,我们就不要在ServerLogger中写writeReplace方法或者让这个方法返回this对象),一旦其中一个方法或者两个方法都没有写,那么java就会在内部调用对应的默认方法来实现相应的功能。
同理,如果我们需要对一个对象进行反序列化的话(比如从一个文件中读取一个对象),我们需要调用ObjectInputStream对象的readObject方法,如下面的例子所示:
FileInputStream fis = new FileInputStream("logger");
ObjectInputStream ois = new ObjectInputStream(fis);
ServerLogger restoredObj = (ServerLogger) ois.readObject();
如果我们在ServerLogger这个类中写了readObject方法和readResolve方法(注意:readObject方法在java内部是通过反射机制查找private方法从而实现对readObject方法进行调用的,所以readObject方法必须是private的),那么当上面最后一行的readObject这个方法被调用的时候,它内部会去依次调用写在ServerLogger中的readObject方法和readResolve方法,如果其中一个函数没有写在ServerLogger的话,java内部会调用对应的默认实现方法,这个方法就是java内部实现好的标准的序列化对象的方法。
所以,ObjectInputStream类的readObject方法的实现是:如果在需要序列化的类的内部写了readObject方法和readResolve方法,那么就先调用readObject方法,然后再调用readResolve方法,返回反序列化后的对象(其实ObjectInputStream中的readObject返回的结果就是readResolve这个方法返回的结果,所以我们为了返回我们刚刚才反序列化的对象,我们通常会让这个方法返回this),一旦其中一个方法或者两个方法都没有写,那么java就会在内部调用对应的默认方法来实现相应的功能。
最后要解释的方法是readObjectNoData,根据java文档,我们可以发现这个方法是在对象流被篡改的时候调用的,这个方法我没有实践过,所以我也不知道是不是这样的。
还有一点需要注意的是:如果上面提到的ServerLogger在不同的JVM(或者说是不同的计算机上)的话,如果我们想要顺利的实现ServerLogger的序列化和反序列化的话,由于这两个类存在于不同的JVM上,所以他们必须要满足下面的两个条件:
(1)两个类都放在同一个包(package)下
(2)两个类中声明serialVersionUID这个常量,且值是一样的(因为java判定两个类是否可以进行序列化的转换就是看这个值是否一样):
private static final long serialVersionUID = 42L;
虽然这个常量前面的private可以是任何的修饰符,但是java文档建议我们使用private,还有就是serialVersionUID的值最好我们自己指定的,虽然可以通过JVM计算出来,但是不同的JVM很难保证算出来的值都是一样的(这里也暴露了用系列化对象在网络中完成传递的一个弊端)。
最后,需要注意的一个细节点是(遇到这个细节的情况并不多):如果B是A的子类,但是A没有实现Serializable接口,当我们利用java默认的序列化和反序列化方法的时候(即不提供上面说的5个方法),java是不会把A进行序列化的而且也不会报任何的异常,在反序列化的时候,java发现B是A的子类,但是A并不存在于对象流中,那么java默认会调用A的无参数的构造函数去先构造A,然后再反序列化构造出B,所以这个时候我们需要给A提供一个无参数的构造函数,如果没有,java会报no valid constructor这个异常。当然如果A也实现了Serializable接口,那么在B被序列化的时候A也会被序列化,对象流中就存在A这个类了,java就会利用对象流将A和B反序列化,而不是构造函数,所以这个时候A就可以没有这个无参数的构造函数。
二、例子
看完了基本原理,我们来举个例子来说明这几个方法的使用和效果。我们要演示的就是上面提到的4个方法(readObjectNoData这个方法不太好演示,我们就只写出它,但是没有效果)的一个典型的应用:当我们将一个类写入到一个文件中的时候,若父类没有实现Serializable这个接口,那么我们如何在读取这个类同时恢复父类的状态呢?因为父类没有实现Serializable这个接口,那么开始将这个类写入到文件中的时候,父类的状态肯定是没有写入的。这个时候其实我们应该很容易的想到在这个类中加入readObject方法来实现父类的回复,其实就是在这个方法中调用super()来重新初始化父类。因为父类需要比子类先初始化,所以我们最好将恢复父类的方法写到readObject中,而不是readResolve中。出于简单的考虑,我就不用前面提到的ServerLogger这个例子了,而是用一个更加简单的例子来说明:
1. 利用java默认的序列化和反序列化方法且父类没有实现Serializable接口:
public class SuperClass {
private int x;
private int y;
public SuperClass(int x, int y) {
this.x = x;
this.y = y;
}
/**
* 如果SuperClass没有实现Serializable接口,那么这个无参数的构造函数必须提供,如果实现了就可以不用提供了。
*/
public SuperClass() {
System.out.println("I am SuperClass");
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
public class SubClass extends SuperClass implements Serializable {
private static final long serialVersionUID = 1L;
private int z;
public SubClass(int x, int y, int z) {
super(x, y);
this.z = z;
}
public int getZ() {
return z;
}
}
public class Driver {
public static void main(String[] args) {
SubClass sc = new SubClass(1,2,3);
try
(
FileOutputStream fos = new FileOutputStream("test");
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream("test");
ObjectInputStream ois = new ObjectInputStream(fis);
) {
oos.writeObject(sc);
SubClass restoredSc = (SubClass) ois.readObject();
System.out.println("x: " + restoredSc.getX());
System.out.println("y: " + restoredSc.getY());
System.out.println("z: " + restoredSc.getZ());
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.toString());
System.exit(1);
}
}
}
输出:
I am SuperClass
x: 0
y: 0
z: 3
我们可以看见父类是通过无参的构造函数创建的,所以父类中的属性值丢失了。
2. 利用java默认的序列化和反序列化方法且父类实现了Serializable接口:
public class SuperClass implements Serializable {
private static final long serialVersionUID = 2L;
private int x;
private int y;
public SuperClass(int x, int y) {
this.x = x;
this.y = y;
}
/**
* 如果SuperClass没有实现Serializable接口,那么这个无参数的构造函数必须提供,如果实现了就可以不用提供了。
*/
public SuperClass() {
System.out.println("I am SuperClass");
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
public class SubClass extends SuperClass implements Serializable {
private static final long serialVersionUID = 1L;
private int z;
public SubClass(int x, int y, int z) {
super(x, y);
this.z = z;
}
public int getZ() {
return z;
}
}
public class Driver {
public static void main(String[] args) {
SubClass sc = new SubClass(1,2,3);
try
(
FileOutputStream fos = new FileOutputStream("test");
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream("test");
ObjectInputStream ois = new ObjectInputStream(fis);
) {
oos.writeObject(sc);
SubClass restoredSc = (SubClass) ois.readObject();
System.out.println("x: " + restoredSc.getX());
System.out.println("y: " + restoredSc.getY());
System.out.println("z: " + restoredSc.getZ());
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.toString());
System.exit(1);
}
}
}
输出:
x: 1
y: 2
z: 3
我们可以看见父类是序列化的对象流创建的,所以父类中的属性值还在。
3. 父类无法实现Serializable接口(但是父类需要给我们提供恢复父类属性值的函数,否则只能用反射来恢复父类的属性值了),利用自定义的序列化和反序列化方式来恢复父类中属性的值:
public class SuperClass {
private int x;
private int y;
public SuperClass(int x, int y) {
this.x = x;
this.y = y;
}
/**
* 如果SuperClass没有实现Serializable接口,那么这个无参数的构造函数必须提供,如果实现了就可以不用提供了。
*/
public SuperClass() {
System.out.println("I am SuperClass");
}
public int getX() {
return x;
}
public int getY() {
return y;
}
protected void setX(int x) {
this.x = x;
}
protected void setY(int y) {
this.y = y;
}
}
public class SubClass extends SuperClass implements Serializable {
private static final long serialVersionUID = 1L;
private int z;
private int x;
private int y;
public SubClass(int x, int y, int z) {
super(x, y);
this.x = x;
this.y = y;
this.z = z;
}
public int getZ() {
return z;
}
private Object writeReplace() throws ObjectStreamException {
return this; // 一般是返回this
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 利用java中默认的方式将非静态的和非transient的变量写到对象流中去
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 利用java中默认的方式将当前对象的非静态和非transient的属性从对象流中读出来
/* 恢复父类的属性值,必须写在defaultReadObject方法后面,因为这个方法需要先读出x和y的值 */
super.setX(x);
super.setY(y);
}
private Object readResolve() throws ObjectStreamException {
return this; // 一般是返回this
}
private void readObjectNoData() throws ObjectStreamException {
System.out.println("readObjectNoData is invoked");
}
}
public class Driver {
public static void main(String[] args) {
SubClass sc = new SubClass(1,2,3);
try
(
FileOutputStream fos = new FileOutputStream("test");
ObjectOutputStream oos = new ObjectOutputStream(fos);
FileInputStream fis = new FileInputStream("test");
ObjectInputStream ois = new ObjectInputStream(fis);
) {
oos.writeObject(sc);
SubClass restoredSc = (SubClass) ois.readObject();
System.out.println("x: " + restoredSc.getX());
System.out.println("y: " + restoredSc.getY());
System.out.println("z: " + restoredSc.getZ());
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.toString());
System.exit(1);
}
}
}
输出:
I am SuperClass
x: 1
y: 2
z: 3
我们可以看到虽然父类还是通过调用无参的构造函数创建的,但是它的属性值却被恢复出来了,具体代码在上面的代码中已经标示出来了。