Java序列化

在一些情况下,如果对象能够在程序不运行的情况下仍能存在并保存其信息,这样在下次运行程序时,该对象将被重建,并且拥有的信息与程序在上次运行时它所拥有的信息相同,这将非常有用。“将一个对象编码成一个字节流”,称为对象序列化,相反的处理过程称为反序列化。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机被传递到令一台虚拟机上,或者被存储到磁盘上,供以后反复使用。Java提供了内建的语言机制来实现序列化,在Java的序列化中涉及到两个接口和一个关键字,分别是Serializable接口和Externalizable接口,以及transient关键字。

Serializable接口

Serializable是一个标记接口,它不包含任何方法,只要对象实现了Serializable接口,对象的序列化就会非常简单。在Java中,序列化是语言特性,几乎不需要我们做什么,虚拟机就会正确的完成对象的序列化。来看一个小小的例子:

public class SerializableDemo implements Serializable {
	private String name;
	private String password;
	//在反序列过程中没有被调用,序列化是语言外的对象创建机制
	public SerializableDemo() {
		System.out.println("Defualt constructor");
	}
	
	public SerializableDemo(String name,String pw) {
		System.out.println("execute SerializableDemo(String name,String pw)");
		this.name = name;
		this.password = pw;
	}
	
	public String toString() {
		return "{ name: " + name +";password: " + password + " }";
	}

	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
		SerializableDemo instance = new SerializableDemo("cxy","123456");
		System.out.println(instance);
		
		//将对象序列化到文件中
		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./bin/com/think/io/serializableDemo.txt"));
		//可以写入基本类型
		out.writeInt(10001);
		//将对象写入
		out.writeObject(instance);
		out.close();
		
		//反序列化,生成对象
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("./bin/com/think/io/serializableDemo.txt"));
		//反序列化得到的对象,必须按写入的顺序读
		int no = in.readInt();
		SerializableDemo deseri = (SerializableDemo) in.readObject();
		System.out.println(deseri);
		
		//生成了不同的对象
		System.out.println("instance == deseri : " + (instance == deseri));
	}

}
在上面的代码中,SerializableDemo类实现了Serializable接口,它有两个private域。我们演示了把对象持久到文件中,然后再从文件中把对象重建,在使用ObjectInputStream时,获取对象(或基本类型)的顺序必须与写入顺序一致。我们还应该注意到,重建的对象和原来的对象是两个不同的对象。实现Serializable接口实现的序列化,是一种默认的自动序列化机制,它会把所有的属性全部序列化,这一切都是自动发生的;在重建的过程中,它没有调用任何构造方法,序列化是一种语言外的对象创建机制。

transient关键字

在前面的例子中,我们看到Serializable接口是一种自动化序列机制,它会把所有的域全部序列化,即使是private的。这样会存在一个问题,我们不希望序列化某些敏感信息(如密码),因为一经序列化,人们就可以通过读取文件或者拦截网络传输的方法访问到它。有一种方法可以防止对象的敏感部分被序列化,那就是实现Externalizable接口,稍后会看到这样的例子。如果在Serializable接口中,我们需要transient(瞬时)关键字的帮忙,当使用transient关键字修饰字段时,它会关闭该字段序列化,它隐含的意思是:我不需要序列化,反序列化时,把我设置零值就行了。比如前面的例子中,password属性敏感信息,为了阻止对它的序列化,使用transient:

private transient String password;
这样在反序列过程中创建的对象deseri,其pawwsord字段将是null。由于Externalizable接口不自动序列化,所以transient关键字只能与Serializable接口配合使用。

Externalizable接口

像上面的例子所列举的那样,如果想对序列化进行更多的控制,使用Serializable接口显然是不合适的,可以实现Externalizable接口来对序列化过程进行控制。Externalizable接口有两个方法,writeExternal()和readExternal(),分别用来控制字段的序列化和反序列化。看一下Externalizable接口:

/**
 * Externalizable继承自Serializable接口
 * 默认不自动序列化
 */
public interface Externalizable extends java.io.Serializable {
    /**
     * 该对象可实现 writeExternal 方法来保存其内容,
     * 它可以通过调用 DataOutput 的方法来保存其基本值,
     * 或调用 ObjectOutput 的 writeObject 方法来保存对象、字符串和数组。
     * @param out 要写入对象的流
     * @throws IOException
     */
    void writeExternal(ObjectOutput out) throws IOException;

    /**
     * 对象实现 readExternal 方法来恢复其内容,它通过调用 DataInput 的方法来恢复其基础类型,
     * 调用 readObject 来恢复对象、字符串和数组。
     * readExternal 方法必须按照与 writeExternal 方法写入值时使用的相同顺序和类型来读取这些值。
     * @param in 为了恢复数据而从中读取数据的流
     * @exception IOException if I/O errors occur
     * @exception ClassNotFoundException If the class for an object being
     *              restored cannot be found.
     */
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
Externalizable将序列化的控制交由程序员来控制,权利意味着义务,程序员必须要做额外的工作才能正确的完成对象的序列化,在上面的例子中,如果把实现Serializable接口改成实现Externalizable接口,而不做额外的控制,那么反序列化得到的对象的name和password字段将是null。为了正确的序列化,必须要在writeExternal()中显式的把需要序列化的字段写入流中,在反序列化时,要在readExternal()中显式的从流中读取字段值。Externalizable的序列化过程和Serializable的不同,如下的代码将展示其中的差异:
public class ExternalizableDemo implements Externalizable {
	//内部类,用来说明字段定义处的初始化在反序列化的过程中也会得到执行
	private static class Helper {
		Helper(String content) {
			System.out.println(content);
		}
	}
	
	private Helper h = new Helper("字段定义处的初始化");
	private String name;
	private String password;
	//在反序列化的过程中,语句块中的初始化会被执行
	{
		System.out.println("******语句块中的内容********");
	}
	//在反序列化的过程中,默认构造方法会被执行
	public ExternalizableDemo() {
		System.out.println("Defualt constructor");
	}
	
	public ExternalizableDemo(String name,String pw) {
		System.out.println("execute SerializableDemo(String name,String pw)");
		this.name = name;
		this.password = pw;
	}
	
	public String toString() {
		return "{ name: " + name +";password: " + password + " }";
	}

	/**
	 * 需要在这里显式的调用writeXXX将字段序列化
	 */
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeObject(name);
	}

	/**
	 * 需要在这里显式的调用readXXX将字段反序列化
	 * 同时把获取的值赋给字段
	 */
	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		name = (String)in.readObject();
	}
	
	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
		ExternalizableDemo instance = new ExternalizableDemo("cxy","123456");
		System.out.println(instance);
		
		//将对象序列化到文件中
		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./bin/com/think/io/external.txt"));
		//可以写入基本类型
		out.writeInt(10001);
		//将对象写入
		out.writeObject(instance);
		out.close();
		System.out.println("\n反序列化开始.\n");
		//反序列化,生成对象
		ObjectInputStream in = new ObjectInputStream(new FileInputStream("./bin/com/think/io/external.txt"));
		//反序列化得到的对象,必须按写入的顺序读
		int no = in.readInt();
		ExternalizableDemo deseri = (ExternalizableDemo) in.readObject();
		System.out.println(deseri);
		
		//生成了不同的对象
		System.out.println("\ninstance == deseri : " + (instance == deseri));
	}
}
上面程序的执行结果:

字段定义处的初始化
******语句块中的内容********
execute SerializableDemo(String name,String pw)
{ name: cxy;password: 123456 }

反序列化开始.

字段定义处的初始化
******语句块中的内容********
Defualt constructor
{ name: cxy;password: null }
instance == deseri : false

我们可以看到,在反序列化的过程中调用了默认构造器,这与反序列Serializable对象的不同。对于Serializable对象,对象完全以它存储的二进制位为基础来构造,而不调用构造器;而对于Externalizable对象,所有的普通的默认构造器都会被调用(无参构造器,字段定义处的初始化,语句块初始化),然后才调用readExternal()来反序列对象。序列化的过程完全在我们的控制中,在上面的例子中,没有将password序列化,那么反序列化得到的对象的password就为null。

我们还应注意到,readExternal()和writeExternal()是自动被调用的。

奇怪的序列化形式

Java提供了一种方式,使程序员拥有更多对Serializable对象序列化的控制,就像对Externalizable对象一样:给实现Serializable接口的对象添加writeObject()和readObject()方法,这样一旦对象被徐立华或者被反序列化时,就会自动地调用这个两个方法,而不是使用默认的序列化机制。这两个方法必须具有准确的方法特征签名:

private void readObject(ObjectInputStream stream) throws IOException;

private void writeObject(ObjectOutputStream stream) throws IOExcpeiton,ClassNotFoundException;

在调用ObjectOutputStream.writeObject(Object obj)时,VM会检查所传递的Serializable对象,看看是否实现了自己的writeObject(ObjectOutputStream stream)方法,如果实现了该方法,就跳过正常的序列化过程并调用它的writeObject(ObjectOutputStream stream);readObject()的过程与此类似。只是添加的这两个方法,与序列化时调用的writeObject()和readObject()方法名称相同,实在是很容易混淆。添加的两个方法都是private,其使用方法与Externalizable接口的writeExternal()和readExternal()类似,下面展示一个使用它的例子:

public class SerializableAdd implements Serializable {
	private String name;
	private String no;
	private transient String password;
	
	//添加的序列化方法
	private void writeObject(ObjectOutputStream stream)throws IOException {
		//普通的字段可以使用默认的序列机制
		//注意:defaultWriteObject()只能在序列化的writeObject()方法中调用
		//stream.defaultWriteObject();
		
		//也可以调用writeXXX()
		stream.writeObject(name);
		stream.writeObject(no);
		
		//对于transient方法就可以显式控制
		stream.writeObject(password);
	}
	
	//添加的反序列化方法
	private void readObject(ObjectInputStream stream)
			throws IOException,ClassNotFoundException {
		//注意:defaultReadObject()只能在序列化的readObject()方法中调用
		stream.defaultReadObject();
		
		password = (String)stream.readObject();
	}
}
实现深拷贝

对象的序列化并仅仅只是保存了目标对象的内容,它能追踪目标对象内所包含的所有引用,并保存那些对象,并且还是递归的追踪,比如A包含B,B包含C,在反序列化A时,A里的B同样会得到反序列化,B里的C也能得到反序列化,只要B和C都实现了Serializable接口或者Externalizable接口。为了实现深拷贝,还有两个问题没有弄明白:反序列化得到的对象与原始对象是同一个对象吗?他们指向的第三个对象引用(上例中的B,C)是相同的吗?下面的程序可以解答这两个问题:

public class ReferenceTest implements Serializable{
	//必需实现Serializable接口,否则不能序列化
	private static class Data implements Serializable{
		private long id = 10001;
		
		public void setId(long id) {
			this.id = id;
		}
		
		public long getId() {
			return id;
		}
	}
	
	private Data d;
	
	public ReferenceTest(Data d) {
		this.d = d;
	}
	
	public Data getD() {
		return d;
	}
	
	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
		Data d = new Data();
		ReferenceTest origin = new ReferenceTest(d);
		System.out.println("origin.hashCode(): " + origin.hashCode());
		//将对象持久到文件中
		ObjectOutputStream out1 = new ObjectOutputStream(
				new FileOutputStream("./bin/com/think/io/seria1.txt"));
		//把同一个对象写入两次
		out1.writeObject(origin);
		out1.writeObject(origin);
		out1.close();
		
		ObjectInputStream in1 = new ObjectInputStream(
				new FileInputStream("./bin/com/think/io/seria1.txt"));
		ReferenceTest deseri = (ReferenceTest)in1.readObject();
		ReferenceTest singleStream = (ReferenceTest) in1.readObject();
		System.out.println("deseri.hashCode(): " + deseri.hashCode());
		
		//原始对象与序列化对象是不同的对象
		System.out.println("origin == deseri: " + (origin == deseri));
		//引用的对象同样不同
		System.out.println("origin.getD() == deseri.getD() : " + (origin.getD() == deseri.getD()));
		//同一个流中序列化的同一个对象,反序列得到的对象相同吗?
		System.out.println("deseri == singleStream: " + (deseri == singleStream));
		
		//把同一个对象序列化到不同的地方,反序列化得到的对象时相同的吗?
		//把origin持久到另一个地方
		ByteArrayOutputStream buff = new ByteArrayOutputStream();
		ObjectOutputStream out2 = new ObjectOutputStream(buff);
		out2.writeObject(origin);
		out2.close();
		
		ByteArrayInputStream buff_in = new ByteArrayInputStream(buff.toByteArray());
		ObjectInputStream in2 = new ObjectInputStream(buff_in);
		ReferenceTest other = (ReferenceTest)in2.readObject();
		System.out.println("other.hashCode(): " + other.hashCode());
		System.out.println("deseri == other: " + (deseri == other));
	}

}
ReferenceTest持有一个Data的引用,注意Data必须实现Serilizable接口,否则不能序列化。根据运行结果,为了更清楚的看到真相,打印了每个对象的hashCode(),如果两个对象是相同的,一下是输出结果:

origin.hashCode(): 20545604
deseri.hashCode(): 18979724
origin == deseri: false
origin.getD() == deseri.getD() : false
deseri == singleStream: true
other.hashCode(): 23804217
deseri == other: false

于是我们得到一下结论:

(1)反序列化得到的对象与原始对象不是同一个对象,这是我们希望得到的结果;

(2)对象中持有的引用对象也是不相同的,这同样是我们想要的结果;

(3)同一个对象持久到不同的流中,并从中反序列化得到的对象也不相同,这是一个合理的结果,一个流不可能知道另一个流里的内容,deseri==other为false。不过同一个流里两次序列的同一个对象,反序列得到的两个对象是相同的,上面例子中deseri==singleStream为true,验证这一条。

ClassNotFound异常

在反序列化的过程中,如果在classpath下找不到反序列化对象对应的.class文件,就会抛出ClassNotFoundException。

以上只是对Java序列化用法的介绍,并不涉及更多深层次的内容,我们可以看到Java的序列化是一个非常有用的特性。

转载请注明:喻红叶《Java序列化》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值