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使用的更多;