[疯狂Java]I/O:其它自定义序列化的方法(transient、writeReplace、readResolve、Externalizable)

1. 一种不是很好的排除序列化——transient关键字:

    1) 如果你不想让对象中的某个成员被序列化可以在定义它的时候加上transient关键字进行修饰,例如:

class A implements Serializable {
    private int a;
    private transient int b;
    ...
!!这样,在A的对象被序列化时其成员b就不会被序列化;

    2) 该关键字可以保证反序列化时是安全正常的,只不过被transient修饰过的成员反序列化后将自动获得0或者null值,上面的b在反序列化后被赋为0;

    3) transient只能用于修饰示例变量,不可修饰其它Java成分;

!!理由很简单,transient只在序列化时起作用,因此修饰方法、类等毫无意义,而且不能修饰静态成员,因为静态成员不参与序列化!!

    4) 这种方法不是很好,理由很简单,因为它将被修饰的实例成员完全排除在序列化机制之外了,在反序列化后如果要求那些成员不能为0或者null,则可能会带来不必要的错误!(误把它们当做非0或者非空来使用!),并且有时候并不希望默认给它们赋0或者null,也许有其它更加符合需求的默认值呢?


2. 解决反序列化异常的神器——Serializable接口的readObjectNoData方法:

    1) Serializable作为标记接口其实还有除了readObject和writeObject之外的其它标记接口方法;

    2) 这里介绍的方法专门用来解决反序列化异常问题,需要自己实现:private void readObjectNoData() throws ObjectStreamException;

    3) 当反序列化是遇到如下异常会自动调用该方法:

         i. 序列化版本不兼容;

         ii. 输入流被篡改或者损坏;

!!该方法的目的就是在发生上面这些异常时给对象提供一个合理的值(比如默认值或者全是0、null之类的,视具体情况而定);

!!但是如果泛序列化时接受的类程序中不存在还是会抛出异常的,毕竟该方法最终还是会还原出一个对象来,而对象的存在是以类的存在位前提的,所以没有类还是不行的!


3. 写入时替换对象——writeReplace:

    1) Serializable还有两个标记接口方法可以实现序列化对象的替换,即writeReplace和readResolve;

!!writeReplace的原型:任意访问限定符 Object writeReplace() throws ObjectStreamException;

    2) 如果实现了writeReplace方法后,那么在序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中,例如:

class Person implements Serializable {
	private String name;
	private int age;
	
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name + "(" + age + ")";
	}
	
	private Object writeReplace() throws ObjectStreamException {
		ArrayList<Object> list = new ArrayList<>();
		list.add(name);
		list.add(age);
		return list;
	}
	
}
!在这里就将该对象直接替换成了一个list保存;

!!!注意:

            a. 实现writeReplace就不要实现writeObject了,因为writeReplace的返回值会被自动写入输出流中,就相当于自动这样调用:writeObject(writeReplace());

            b. 因此writeReplace的返回值(对象)必须是可序列话的,如果是Java自己的基础类或者类型那就不用说了;

            c. 但如果返回的是自定义类型的对象,那么该类型必须是彻底实现序列化的!

    3) writeReplace的替换如何在反序列化时被恢复?

         i. 注意!不是用readResolve恢复哦!readResolve并不是用来恢复writeReplace的!

         ii. 这里无法恢复了!即对象被彻底替换了!也就是说使用ObjectInputStream读取的对象只能是被替换后的对象,只能在读取后自己手动恢复了,接着上例的演示:

class Person implements Serializable {
	private String name;
	private int age;
	
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name + "(" + age + ")";
	}
	
	private Object writeReplace() throws ObjectStreamException {
		ArrayList<Object> list = new ArrayList<>();
		list.add(name);
		list.add(age);
		return list;
	}
}

public class Test {
	
	public static void print(String s) {
		System.out.println(s);
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.buf"))) {
			Person p = new Person("lala", 33);
			oos.writeObject(p);
			oos.close();
		}
		try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.buf"))) {
			print(((ArrayList)ois.readObject()).toString());
		}
	}
}
!会打印出"[lala, 33]";

    4) 使用writeReplace替换写入后也不能通过实现readObject来实现自动恢复了,即下面的代码是错误的!

class Person implements Serializable {
	private String name;
	private int age;
	
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name + "(" + age + ")";
	}
	
	private Object writeReplace() throws ObjectStreamException {
		ArrayList<Object> list = new ArrayList<>();
		list.add(name);
		list.add(age);
		return list;
	}
	
	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { // 错!写了也没用
		ArrayList<Object> list = (ArrayList)in.readObject();
		this.name = (String)list.get(0);
		this.age = (int)list.get(1);
	}
}
!!因为默认已经被彻底替换了,就不存在自定义反序列化的问题了,直接自动反序列化成ArrayList了,该方法即使实现了也不会调用!!

    5) 因此得到一个结论,那就是writeObject只和readObject配合使用,一旦实现了writeReplace在写入时进行替换就不再需要writeObject和readObject了!因为替换就已经是彻底的自定义了,比writeObject/readObject更彻底!



4. 保护性恢复对象(同时也可以替换对象)——readResolve:

    1) readResolve会在readObject调用之后自动调用,它最主要的目的就是让恢复的对象变个样,比如readObject已经反序列化好了一个Person对象,那么就可以在readResolve里再对该对象进行一定的修改,而最终修改后的结果将作为ObjectInputStream的readObject的返回结果;

    2) 原型:任意访问限定符 Object readResolve() throws ObjectStreamException;

    3) 该方法起到的作用:

         i. 调用该方法之前会先调用readObject反序列化得到对象;

         ii. 接着,如果该方法存在则会自动调用该方法;

         iii. 在该方法中可以正常通过this访问到刚才反序列化得到的对象的内容;

         iv. 然后可以根据这些内容进行一定处理返回一个对象;

         vi. 该对象将作为ObjectInputStream的readObject的返回值(即该对象将作为对象输入流的最终输入);

!!可以看到,你可以返回一个非原类型的对象,也就是说可以彻底替换对象

    4) 示例:

class Person implements Serializable {
	private String name;
	private int age;
	
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return name + "(" + age + ")";
	}
	
	private Object readResolve() throws ObjectStreamException { // 直接替换成一个int的1返回
		return 1;
	}
}

public class Test {
	
	public static void print(String s) {
		System.out.println(s);
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.buf"))) {
			Person p = new Person("lala", 33);
			oos.writeObject(p);
			oos.close();
		}
		try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.buf"))) {
			print(ois.readObject().toString());
		}
	}
}
!看到打印出了1;

!!当然你可以对反序列化的对象进行修改再返回,这样返回的还是原类型,还是要根据具体需要来决定;

    5) 那么readResolve的用武之地到底是什么呢?这样在反序列化时无谓的替换很无聊的:

         i. 其最重要的应用就是保护性恢复单例、枚举类型的对象!

         ii. 这里举个例子:用单例模式构造一个枚举类型,然后对其进行序列化和反序列化

class Brand implements Serializable {
	private int val;
	private Brand(int val) {
		this.val = val;
	}
	
	// 两个枚举值
	public static final Brand NIKE = new Brand(0);
	public static final Brand ADDIDAS = new Brand(1);
}

public class Test {
	
	public static void print(String s) {
		System.out.println(s);
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.buf"))) {
			oos.writeObject(Brand.NIKE);
			oos.close();
		}
		try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.buf"))) {
			Brand b = (Brand)ois.readObject();
			print("" + (b == Brand.NIKE)); // 答案显然是false
		}
	}
}
!!答案很显然是false,因为Brand.NIKE是程序中创建的对象,而b是从磁盘中读取并恢复过来的对象,两者明显来源不同,因此必然内存空间是不同的,引用(地址)显然也是不同的;

!!但这不是我们想看到的,因为我们把Brand设计成枚举类型,不管是程序中创建的还是从哪里读取的,其必须应该和枚举常量完全相等,这才是枚举的意义啊!

         iii. 而此时readResolve就派上用场了,我们可以这样实现readResolve:

class Brand implements Serializable {
	private int val;
	private Brand(int val) {
		this.val = val;
	}
	
	// 两个枚举值
	public static final Brand NIKE = new Brand(0);
	public static final Brand ADDIDAS = new Brand(1);
	
	private Object readResolve() throws ObjectStreamException {
		if (val == 0) {
			return NIKE;
		}
		if (val == 1) {
			return ADDIDAS;
		}
		return null;
	}
}
!!这样后,不管来源如何,最终得到的都将是程序中Brand的枚举值了!因为readResolve的代码在执行时已经进入了程序内存环境,因此其返回的NIKE和ADDIDAS都将是Brand的静态成员对象;

        iv. 因此保护性恢复的含义就在此:首先恢复的时候没有改变其值(val的值没有改变)同时恢复的时候又能正常实现枚举值的对比(地址也完全相同);

    6) 小结:readResolve的最主要应用场合就是单例、枚举类型的保护性恢复!

!!当然自己手动实现的单例、枚举类型要串行化是必须要实现readResolve的保护性恢复的,但是如果使用Java的enum关键字来定义枚举类型则不需要了(Java 5之后的版本都实现了enum类型的自动保护性恢复,但是Java 5之前的老版本还是不行!);


5. 强制自己实现串行化和反串行化算法——Externalizable接口:

    1) 不像Serializable接口只是一个标记接口,里面的接口方法都是可选的(可实现可不实现,如果不实现则启用其自动序列化功能),而Externalizable接口不是一个标记接口,它强制你自己动手实现串行化和反串行化算法:

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException; // 串行化
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; // 反串行化
}
!!可以看到该接口直接继承了Serializable接口,其两个方法其实就对应了Serializable的writeObject和readObject方法,实现方式也是一模一样;

    2) ObjectOutput和ObjectInput的使用方法和ObjectOutputStream和ObjectInputStream一模一样(readObject、readInt之类的,完全一模一样);

    3) 因此Externalizable就是强制实现版的Serializable罢了;

    4) 示例:

class Person implements Externalizable {
	private String name;
	private int age;
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		// TODO Auto-generated method stub
		out.writeObject(new StringBuffer(name).reverse());
		out.writeInt(age);
	}
	@Override
	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		// TODO Auto-generated method stub
		this.name = ((StringBuffer)in.readObject()).reverse().toString();
		this.age = in.readInt();
	}
	
}
!用法上和Serializable的readObject/writeObject没两样;

    5) 和Serializable的主要区别:

         i. Externalizable更加高效一点;

         ii. 但Serializable更加灵活,其最重要的特色就是可以自动序列化,这也是其应用更广泛的原因;

         iii. 如果真的需要自己定义序列化并且对效率要求较高,那么Externalizable是你的首选,但通常情况下Serializable使用的更多;

阅读更多
个人分类: 疯狂Java笔记
上一篇[疯狂Java]I/O:I/O流的最高境界——对象流(序列化:手动序列化、自动序列化、引用序列化、版本)
下一篇[疯狂Java]I/O:NIO简介、Buffer
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭