黑马程序员-----IO之深入对象序列化

---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------

1.对象引用的序列化
前面介绍的Person类的两个属性分别是String类型和int类型,如果某个类的属性类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型属性的类是不可序列化的。
如下Teacher类持有一个Person类的引用,则只有当Person类是可序列化的,Teacher类才是可序列化的。如果Person不可序列化,则无论Teacher是否实现Serializable、Externalizable接口,它是不可序列化的。
public class Teacher implements Serializable{

	//属性
	private String name;
	//属性类型是引用类型Person类
	private Person student;
	
	//构造器
	public Teacher(){}
	public Teacher(String name, Person student){
		this.name = name;
		this.student = student;
	}
	
	//setter和getter
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Person getStudent() {
		return student;
	}
	public void setStudent(Person student) {
		this.student = student;
	}
	
}

注:程序序列化一个Teacher对象时,如果该Teacher对象持有一个Person对象的引用,为了在反序列化时可以正常恢复该Teacher对象,则程序会顺带将该Person对象也进行序列化,所以Person也必须是可序列化的,否则Teacher类将不可序列化。

现在假设有如下一种情形:程序中有两个Teacher对象,他们的student属性都引用到同一个Person对象,而且该Person对象还有一个引用变量引用他。如下:
Person student = new Person("郭靖", 24);
Teacher t1 = new Teacher("柯镇恶", student);
Teacher t2 = new Teacher("洪七公", student);
在内存中示意图如下图


这里产生了一个问题,如果先序列化t1对象,则系统将t1对象所引用的Person对象序列化;如果程序再序列化t2对象,系统将再次序列化t2对所引用的Person对象,如果程序再显示序列化student对象,那么将再次序列化该Person对象。这个过程似乎会向输出流中输出3个Person对象。如果系统将输出流中写入了3个Person对象,那后果是程序从输入流中反序列这些对象时,将会得到3个Person对象,从而引起t1和t2所引用的Person对象不是同一个对象,这违背了Java的设计机制。

所以Java序列化机制采用了一种特殊的序列化算法,其内容是:
1)所有保存到磁盘中的对象都有一个序列化编号。
2)当程序试图序列化一个对象时,程序先将检查该对象是否已经被序列化过,只有当该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。
3)如果某个对象是已经序列化过的,程序将直接只输出一个序列化编号,而不是再次重新序列化该对象。

根据上面的序列化算法,我们可以得到一个结论,当第二次、第三次序列化Person对象时,程序不会再次将Person对象转换成字节序列并输出,而是仅仅输出一个序列化编号。
假设有如下顺序的序列化代码:
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(student);
那么序列化后磁盘文件的存储示意图如下图:



下面程序序列化了两个Teacher对象,而两个Teacher对象都持有一个引用到同一个Person对象的引用,而且程序两次调用writeObject输出同一个Teacher对象
public class WriteTeacher {

	public static void main(String[] args) {

		ObjectOutputStream oos = null;
		try{
			//创建一个ObjectOutputStream输出流
			oos = new ObjectOutputStream(new FileOutputStream("e:\\teacher.txt"));
			Person student = new Person("郭靖", 24);
			Teacher t1 = new Teacher("柯镇恶", student);
			Teacher t2 = new Teacher("洪七公", student);
			
			//依次将四个对象写入输出流
			oos.writeObject(t1);
			oos.writeObject(t2);
			oos.writeObject(student);
			oos.writeObject(t2);
		}catch(IOException e){
			e.printStackTrace();
		}finally{
			try{
				if(oos != null)
					oos.close();
			}catch(IOException e){
				e.printStackTrace();
			}
		}
	}

}

上面程序四次调用了writeObject方法来输出对象,实际上只是序列化了3个对象,而且程序的两个Teacher对象的student引用实际上是同一个Person对象。下面程序即可证明。
public class ReadTeacher {

	public static void main(String[] args) {

		ObjectInputStream ois = null;
		try{
			//创建一个ObjectInputStream输出流
			ois = new ObjectInputStream(new FileInputStream("e:\\teacher.txt"));
			
			//依次读取输入流中四个对象
			Teacher t1 = (Teacher)ois.readObject();
			Teacher t2 = (Teacher)ois.readObject();
			Person student = (Person)ois.readObject();
			Teacher t3 = (Teacher)ois.readObject();
			
			//输出true
			System.out.println("t1的student和student是否相同:" + (t1.getStudent() == student)); 
			//输出true
			System.out.println("t2的student和student是否相同:" + (t2.getStudent() == student)); 
			//输出true
			System.out.println("t2和t3是否是同一个对象:" + (t2 == t3)); 
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				if(ois != null)
					ois.close();
			}catch(IOException e){
				e.printStackTrace();
			}
		}
	}

}

上面程序依次读取了序列化文件中的四个Java对象,但通过后面比较判断,可以发现t2h和t3是同一个对象,t1的student引用、t2的student引用和student变量的引用是同一个对象。

由于序列化机制,存在一个潜在的问题:当程序序列化一个可变对象时,程序只有在第一次使用writeObject方法输出时才会将该对象转换成字节序列并输出,即使后面该对象的属性已被改变,当程序再次调用writeObject方法时,程序只是输出前面的序列化编号,所以改变的属性值不会被输出。
public class SerializeDemo {

	public static void main(String[] args) {

		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;
		try{
			//创建一个ObjectOutputStream输出流
			oos = new ObjectOutputStream(new FileOutputStream("e:\\ser.txt"));
			Person student = new Person("郭靖", 24);
			//写入输出流
			oos.writeObject(student);
			//改变student对象的name属性
			student.setName("黄蓉");
			//将改变后的对象写入输出流
			//因为系统只是输出序列化编号,所以改变后的name不会被序列化
			oos.writeObject(student);
			
			ois = new ObjectInputStream(new FileInputStream("e:\\ser.txt"));
			Person s1 = (Person)ois.readObject();
			Person s2 = (Person)ois.readObject();
			//输出true,即反序列化后s1等于s2,是同一个对象
			System.out.println("s1==s2:" + (s1 == s2));
			System.out.println(s2.getName());
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				if(oos != null)
					oos.close();
			}catch(IOException e){
				e.printStackTrace();
			}
			
			try{
				if(ois != null)
					ois.close();
			}catch(IOException e){
				e.printStackTrace();
			}
		}
	}

}

当使用Java序列化机制序列化可变对象时一定要注意,只有当第一次调用writeObject方法来输出对象时才会将对象转换成字节序列,并写到ObjectOutputStream;在后面程序中如果该对象的属性发生了改变,即再次调用ObjectOutputStream方法输出该对象时,改变后的属性不会被输出。


2.自定义序列化
在一些特殊的场景下,如果某个类里包含的属性值是敏感信息,例如银行账户信息等,这时候不希望系统将该属性值进行序列化;或者某个属性的类是不可序列化的,因此不希望对该属性进行序列化。
这时候通过在属性前面加上transient关键字,可以指定Java序列化时无须理会该属性值。关键字transient只可以修饰属性。
下面Person类在age属性上加上transient修饰符。
public class Person implements Serializable{

	//属性
	private String name;
	private transient int age;
	
	//构造器
	Person(){}
	Person(String name, int age){
		this.name = name;
		this.age = age;
	}
	
	//setter和getter方法
	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;
	}
	
}

先序列化一个Person对象,然后再反序列化该Person对象,得到反序列化的Person对象后输出该对象的age值。
public class TransientDemo {

	public static void main(String[] args) {

		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;
		try{
			//创建一个ObjectOutputStream输出流
			oos = new ObjectOutputStream(new FileOutputStream("e:\\ser.txt"));
			Person student = new Person("郭靖", 24);
			//写入输出流
			oos.writeObject(student);
			
			ois = new ObjectInputStream(new FileInputStream("e:\\ser.txt"));
			Person s = (Person)ois.readObject();
			System.out.println(s.getAge());
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				if(oos != null)
					oos.close();
			}catch(IOException e){
				e.printStackTrace();
			}
			
			try{
				if(ois != null)
					ois.close();
			}catch(IOException e){
				e.printStackTrace();
			}
		}
	
	}

}

由于Person类的age属性使用transient关键字修饰,所以反序列化后得到age值为0。
使用transient修饰的属性将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时无法取得该属性值。

Java还提供了另一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各属性,甚至完全不序列化某些属性(与使用transient关键字效果相同)。
在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化:

private void writeObject(java.io.ObjectOutputStream out)throws IOException;
private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException;
private void readObjectNoData()throws ObjectStreamException

writeObject方法负责写入特定类的实例的状态,以便相应的readObject方法可以恢复它。通过重写该方法,程序员可以完全获得对序列化机制的控制,程序员可以自主决定哪些属性需要序列化,需要怎样序列化。默认情况下,该方法会调用out.defaultWriteObject来保存Java对象的各属性,从而可以实现序列化Java对象状态的目的。
readObject方法负责从流中读取并恢复对象属性,通过重写该方法,程序员可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些属性,以及进行怎样的反序列化。默认情况下,该方法会调用in.defaultReadObject来恢复Java对象的非static和非transient属性。通常情况下,readObject方法中对其属性进行相应的反处理,以便正确恢复该对象。
当序列化流不完整的时候,readObjectNoData方法可以用来正确地初始化反序列化的对象。例如接收方使用的反序列化类的版本不同于发送方,或者接收着版本扩展的类不是发送者版本扩展的类时,或者序列化流被篡改时,系统都会调用readObjectNoData方法来初始化反序列化的对象。

下面的Person类提供了writeObject和readObject两个方法,其中writeObject方法在保存Person对象时将其name属性包装成StringBuffer,并将其字符序列反转后写入;在readObject方法中读取name属性时先将读取的数据强制类型转换成StringBuffer,然后将其反转后赋给name属性。
public class Person implements Serializable{

	//属性
	private String name;
	private int age;
	
	//构造器
	Person(){}
	Person(String name, int age){
		this.name = name;
		this.age = age;
	}
	
	//setter和getter方法
	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;
	}
	
	//实现自定义序列化
	private void writeObject(java.io.ObjectOutputStream out)
	throws IOException{
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	
	private void readObject(java.io.ObjectInputStream in)
	throws IOException, ClassNotFoundException{
		this.name = ((StringBuffer)in.readObject()).reverse().toString();
		this.age = in.readInt();
	}
	
}

对Person对象序列化和反序列化的程序与前面程序没有任何区别。
public class Demo {

	public static void main(String[] args) {

		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;
		try{
			//创建一个ObjectOutputStream输出流
			oos = new ObjectOutputStream(new FileOutputStream("e:\\ser.txt"));
			Person student = new Person("郭靖", 24);
			//写入输出流
			oos.writeObject(student);
			
			ois = new ObjectInputStream(new FileInputStream("e:\\ser.txt"));
			Person s = (Person)ois.readObject();
			System.out.println(s.getName()+ "---" + s.getAge());
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				if(oos != null)
					oos.close();
			}catch(IOException e){
				e.printStackTrace();
			}
			
			try{
				if(ois != null)
					ois.close();
			}catch(IOException e){
				e.printStackTrace();
			}
		}
	
	}

}


还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象。如果需要实现序列化某对象时替换该对象,应为序列化类提供如下特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此writeReplace方法将由序列化机制调用,前提是此方法存在。子类有可能获得该方法。

下面Person类提供了writeReplace方法,这样可以在写入Person对象时将该对象替换成ArrayList
public class Person implements Serializable{

	//属性
	private String name;
	private int age;
	
	//构造器
	Person(){}
	Person(String name, int age){
		this.name = name;
		this.age = age;
	}
	
	//setter和getter方法
	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;
	}
	
	//重写writeReplace方法
	private Object writeReplace() throws ObjectStreamException{
		ArrayList<Object> list = new ArrayList<Object>();
		list.add(name);
		list.add(age);
		return list;
	}
	
}

Java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace方法,如果该方法返回另一个Java对象,则系统转为序列化另一个对象。
public class Demo {

	public static void main(String[] args) {

		ObjectOutputStream oos = null;
		ObjectInputStream ois = null;
		try{
			//创建一个ObjectOutputStream输出流
			oos = new ObjectOutputStream(new FileOutputStream("e:\\ser.txt"));
			Person student = new Person("郭靖", 24);
			//写入输出流
			oos.writeObject(student);
			
			ois = new ObjectInputStream(new FileInputStream("e:\\ser.txt"));
			ArrayList list = (ArrayList)ois.readObject();
			System.out.println(list);
		}catch(Exception e){
			e.printStackTrace();
		}finally{
			try{
				if(oos != null)
					oos.close();
			}catch(IOException e){
				e.printStackTrace();
			}
			
			try{
				if(ois != null)
					ois.close();
			}catch(IOException e){
				e.printStackTrace();
			}
		}
	
	}

}

系统在序列化某个对象之前,先会调用该对象的如下两个方法:writeReplace和writeObject,系统总是先调用被序列化对象的writeReplace方法,如果该方法返回另一个对象,系统将再次调用另一个对象的writeReplace方法...知道该方法不再返回另一个对象为止,程序最后将调用该对象的writeObject方法来保存该对象的状态。

与writeReplace方法相对的是,序列化机制里还有一个特殊的方法,他可以实现保护性复制整个对象,即:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
这个方法会紧接着readObject之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject反序列化的对象将会被立即丢弃。
readResolve方法在序列化单例类、枚举类时尤其有用。如果使用JDK1.5提供的enum来定义枚举类完全无须担心,程序没有问题。但如果应用中有早起遗留下的枚举类,如下:
public class Orientation {

	public static final Orientation HORIZONTAL = new Orientation(1);
	public static final Orientation VERTICAL = new Orientation(2);
	private int value;
	private Orientation(int value){
		this.value = value;
	}
}

如果将Orientation.HORIZONTAL值序列化后再读出
		//创建一个ObjectOutputStream输出流
		oos = new ObjectOutputStream(new FileOutputStream("e:\\ser.txt"));
		//写入Orientation.HORIZONTAL值
		oos.writeObject(Orientation.HORIZONTAL);
		
		//创建一个ObjectInputStream输入流
		ois = new ObjectInputStream(new FileInputStream("e:\\ser.txt"));
		//读取刚刚序列化的值
		Orientation o = (Orientation)ois.readObject();

如果用o和Orientation.HORIZONTAL值进行比较,返回false!也就是说o是一个新的Orientation对象,不等于其类中的任何枚举值。
所以要通过为该类提供一个readResolve方法来解决该问题。
public class Orientation {

	public static final Orientation HORIZONTAL = new Orientation(1);
	public static final Orientation VERTICAL = new Orientation(2);
	private int value;
	
	private Orientation(int value){
		this.value = value;
	}
	
	//为枚举类增加readResolve方法
	private Object readResolve() throws ObjectStreamException{
		if(value == 1){
			return HORIZONTAL;
		}
		if(value == 2){
			return VERTICAL;
		}
		return null;
	}
}

所有单例类、枚举类在实现序列化时都应该提供readResolve方法,这样才可以保证反序列化的对象依然正常。




---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------详细请查看: http://edu.csdn.net
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值