Java 对象序列化

Java 对象序列化

Java 的对象序列化将那些实现了 Serializable 接口的对象转换成一个字节序列(因此,只能使用 Stream 相关类进行操作),并能够在以后将这个字节序列完全恢复为原来的对象。只要对象实现了 Serializable 接口(该接口仅是一个标记接口,并不包括任何方法),对象序列化和反序列化通过以下两步实现:

  • 创建 OutputStream 对象,封装在 ObjectOutputStream 对象中,只需调用 writerObject 即可将对象序列化
  • 创建 InputStream 对象,封装在 ObjectInputStream 对象中,只需调用 readObject 即可将对象序列化

import java.io.*;

public class Alien implements Serializable {} /* Output 
错误: 在类 Alien 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
*///:~
import java.io.*;

public class FreezeAlien {
	public static void main(String[] args) throws IOException {
		ObjectOutput out = new ObjectOutputStream(
		 new FileOutputStream("X.file"));
		Alien quellek = new Alien();
		out.writeObject(quellek);
	}
}///:~

import java.io.*;

public class ThawAlien {
	public static void main(String[] args) throws Exception {
		ObjectInputStream in = new ObjectInputStream(
		 new FileInputStream(new File("X.file")));
		Object mystery = in.readObject();
		System.out.println(mystery.getClass());
	}
}/* Output 
class Alien
*///:~

但此时如果要想打开和读取序列化对象中的内容就需要 Alien.class 文件;否则,将得到一个 ClassNotFoundException 的异常。

最后需要特别注意的是:在对对象序列化时,文件中不仅保存着对象中的所有数据,还包括版本、 package 等对象的所有信息。因此在反序列化时,必须保证所有环境完全相同才能正确反序列化。比如库版本不一致,将造成 serialVersionUID 不一致。

序列化控制

如果处于安全考虑,不希望对象的某一部分被序列化;或者一个对象被还原后,某个子对象需要重新创建,从而不必将该子对象序列化。Externalizable 接口继承了 Serializable 接口,并定义了 writeExternal 和 readExternal 两个方法,来对序列化和反序列化进行控制。其实现了没有任何东西可以自动序列化,并且只能通过在 writeExternal 方法中对所需部分进行显式序列化。

在反序列化时,所有普通的默认构造器都会被调用(包括在字段定义时的初始化,因此类中必须定义有默认构造器),然后调用 readExternal。必须注意一点——所有默认的构造器都会被调用,才能使反序列化对象产生正确的行为。并且在 writeExternal 中写入的数据必须在 readExternal 中以同样的顺序读出。

同时,如果从一个 Externalizable 对象继承,通常需要在 writeExternal 方法中将来自对象的重要信息写入,还必须在 readExternal 方法中恢复数据。


import java.io.*;

public class Blip implements Externalizable {
	private int i;
	private String s; // No initialization
	
	public Blip() {
		System.out.println("Blip Constructor");
		// s, i not initialized
	}
	
	public Blip(String x, int a){
		System.out.println("Blip(String x, int a)");
		s = x;
		i = a;
		// s & i initialized only in non-default constuctor
	}
	
	public String toString() {return s + i;}
	
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		System.out.println("Blip writeExternal");
		out.writeObject(s);
		out.writeInt(i);
	}
	
	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		System.out.println("Blip readExternal");
		s = (String)in.readObject();
		i = in.readInt();
	}
	
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		System.out.println("Constructing Objects: ");
		Blip b = new Blip("A String", 47);
		System.out.println(b);
		ObjectOutputStream o = new ObjectOutputStream(
		 new FileOutputStream("Blip.out"));
		System.out.println("Saving object: ");
		o.writeObject(b);
		o.close();
		// Now get it back:
		ObjectInputStream in = new ObjectInputStream(
		 new FileInputStream("Blip.out"));
		System.out.println("Recovering b: ");
		b = (Blip)in.readObject();
		System.out.println(b);
	}
}/* Output 
Constructing Objects:
Blip(String x, int a)
A String47
Saving object:
Blip writeExteranl
Recovering b:
Blip Constructor
Blip readExteranl
A String47
*///:~

serializable 中的序列化控制

但当只有某个域(比如密码)不需要被序列化,这样使用 Externalizable 就显得很麻烦;此时可以在实现了 Serializable 的类中使用 transient 关键字来标识某个字段,这样该字段就不会被序列化,同时在反序列化中被赋值为空。

除了关键字,还可以通过在实现了 Serializable 的类中添加 readObject 和 writeObject 来实现来序列化控制,注意此时的添加,并不是覆盖或实现,且必须按以下方式进行特征签名

  • private void writeObject(ObjectOutputStream stream) throws IOException;
  • private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;

从设计的角度出发,情况变得有些扑朔迷离。首先,大家可能认为这些方法不属于基础类或者 Serializable 接口的一部分,所以它们应该在自己的接口中得到定义。但请注意它们被定义成“private”,这意味着它们只能由这个类的其他成员调用。然而,我们实际并不从这个类的其他成员中调用它们,而是由 ObjectOutputStream 和 ObjectInputStream 的 writeObject() 及 readObject() 方法来调用我们对象的 writeObject() 和 readObject() 方法(注意我在这里用了很大的抑制力来避免使用相同的方法名——因为怕
混淆)。大家可能奇怪 ObjectOutputStream 和 ObjectInputStream 如何有权访问我们的类的 private 方法——只能认为这是序列化机制玩的一个把戏。

在任何情况下,接口中的定义的任何东西都会自动具有 public 属性,所以假若 writeObject() 和 readObject() 必须为 private,那么它们不能成为接口的一部分。但由于我们准确地加上了签名,所以最终的效果实际与实现一个接口是相同的。

看起来似乎我们调用 ObjectOutputStream.writeObject() 的时候,我们传递给它的 Serializable 对象似乎会被检查是否实现了自己的 writeObject()。若答案是肯定的是,便会跳过常规的序列化过程,并调用 writeObject()。readObject()也会遇到同样的情况。还存在另一个问题。在我们的 writeObject() 内部,可以调用 defaultWriteObject(),从而决定采取默认的 writeObject() 行动。类似地,在 readObject() 内部,可以调用defaultReadObject()。

但在具体实现以前,有些问题是必须解决的。如果两个对象都有指向第三个对象的句柄,该如何对这两个对象序列化呢?如果从两个对象序列化后的状态恢复它们,第三个对象的句柄只会出现在一个对象身上吗?如果将这两个对象序列化成独立的文件,然后在代码的不同部分重新装配它们,又会得到什么结果呢?

  • 只要将所有东西都序列化到单独一个数据流里,就能恢复获得与以前写入时完全一样的对象网,不会不慎造成对象的重复。当然,在写第一个和最后一个对象的时间之间,可改变对象的状态,但那必须由我们明确采取操作——序列化时,对象会采用它们当时的任何状态(包括它们与其他对象的连接关系)写入。若想保存系统状态,最安全的做法是当作一种“微观”操作序列化。如果序列化了某些东西,再去做其他一些工作,再来序列化更多的东西,以此类推,那么最终将无法安全地保存系统状态。相反,应将构成系统状态的所有对象都置入单个集合内,并在一次操作里完成那个集合的写入。这样一来,同样只需一次方法调用,即可成功恢复之。 

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值