在一些特殊的场景下,如果一个类里包含的某些实例变量是敏感信息,例如银行账户信息,这时不希望系统将该实例变量值进行实例化;或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归实例化,以避免引发异常。
通过在实例变量前面使用transient关键字修饰,可以指定java序列化时无须理会该实例变量。如下Person类与前面的Person类几乎完全一样,只是它的age使用了transient关键字修饰。
1 public class Person implements Serializable 2 { 3 private String name; 4 //transient只能修饰实例变量,不可修饰java程序中的其他成分 5 private transient int age; 6 7 //此处没有提供无参构造 8 public Person(String name, int age) 9 { 10 System.out.println("有参数的构造器"); 11 this.name = name; 12 this.age = age; 13 } 14 15 public String getName() { 16 return name; 17 } 18 19 public void setName(String name) { 20 this.name = name; 21 } 22 23 public int getAge() { 24 return age; 25 } 26 27 public void setAge(int age) { 28 this.age = age; 29 } 30 31 }
下面程序先序列化一个Person对象,然后再反序列化该Person对象,得到反序列化的Person对象后程序输出该对象的age实例变量值。
1 public class TransientTest 2 { 3 public static void main(String[] args) 4 { 5 try( 6 //创建一个ObjectOutputStream输出流 7 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("transient.txt")); 8 //创建一个ObjectInputStream输入流 9 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("transient.txt"))) 10 { 11 Person per = new Person("孙悟空",500); 12 //系统将per对象转换成了字节序列输出 13 oos.writeObject(per); 14 Person p = (Person) ois.readObject(); 15 System.out.println(p.getAge()+""+p.getName()); 16 } 17 catch(Exception ex) 18 { 19 ex.printStackTrace(); 20 } 21 22 } 23 24 }
上面程序分别为Person对象的两个实例变量指定了值。由于本程序中的Preson类的age实例变量使用transient关键字修饰,所以程序代码将输出0;
使用transient关键字修饰实例变量虽然简单方便,但被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对象的非瞬态实例变量。在通常情况下,readObject()方法与writeObject()方法对应,如果writeObject()方法中对java对象的实例变量进行了一些处理,则应该在readObject()方法中对其实例变量进行相应的反处理,以便正确恢复该对象。
当序列化流不完整时,readObjectNoData()方法可以用来正确的初始化反序列化的对象。例如,接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化对象。
下面的Person类提供了writeObject()和readObject()两个方法,其中writeObject()方法在保存Person对象时将其name实例变量包装成StringBuffer,并将其字符序列反转后写入;在readObject()方法中处理name的策略与此对应,先将读取的数据强制类型转换成StringBuffer,再将其反转后赋给name实例。
1 public class Person implements Serializable 2 { 3 private String name; 4 private int age; 5 //此处没有提供无参构造 6 public Person(String name,int age) 7 { 8 System.out.println("有参数的构造器"); 9 this.name = name; 10 this.age = age; 11 } 12 public String getName() { 13 return name; 14 } 15 public void setName(String name) { 16 this.name = name; 17 } 18 public int getAge() { 19 return age; 20 } 21 public void setAge(int age) { 22 this.age = age; 23 } 24 25 26 27 private void writeObject(java.io.ObjectOutputStream out) throws IOException 28 { 29 //将name实例变量值反转后写入二进制流 30 out.writeObject(new StringBuffer(name).reverse()); 31 out.writeInt(age); 32 } 33 34 private void readObject(java.io.ObjectInputStream in) throws Exception 35 { 36 //将读取的字符串反转后赋给name变量 37 this.name = ((StringBuffer)in.readObject()).reverse().toString(); 38 this.age = in.readInt(); 39 } 40 }
上面程序中的方法用以实现自定义序列化,对于这个Preson类而言,序列化,反序列化Preson实例并没有什么区别,去别在于序列化后的对象流,即使有Cracker截获到Person对象流,他看到的name也是加密后的name值,这样就提高了序列化的安全性。
还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象。如果需要实现序列化某个对象时替换该对象,则应为序列化类提供如下特殊方法。
ANY-ACCESS-MODIFIER Object writeReplace()
此writeReplace()方法将由序列化机制调用,只要该方法存在。因为该方法可以拥有私有(private),受保护的(protected),和包私有(package-private)等访问权限,所以其子类有可能获得该方法。例如下面的Person类提供了writeReplace()方法,这样可以在写入Person对象时将该对象替换成ArrayList.
1 public class Person implements Serializable 2 { 3 private String name; 4 private int age; 5 //注意此处没有提供无参构造 6 public Person(String name, int age) 7 { 8 System.out.println("带参构造器"); 9 this.name = name; 10 this.age = age; 11 } 12 public String getName() { 13 return name; 14 } 15 public void setName(String name) { 16 this.name = name; 17 } 18 public int getAge() { 19 return age; 20 } 21 public void setAge(int age) { 22 this.age = age; 23 } 24 25 //重写writeReplace方法,程序在序列化该对象之前,先调用该方法 26 private Object writeReplace() 27 { 28 ArrayList<Object> list = new ArrayList<Object>(); 29 list.add(name); 30 list.add(age); 31 return list; 32 33 } 34 35 }
java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace()方法,如果该方法返回另一个java对象,则系统转为序列化另一个对象。如下程序表面上是序列化Preson对象,但实际上序列化的是ArrayList.
1 public class ReplaceTest 2 { 3 public static void main(String[] args) 4 { 5 try( 6 //创建一个ObjectOutputStream输出流 7 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("replace.txt")); 8 //创建一个ObjectInputStream输入流 9 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("replace.txt"))) 10 { 11 Person per = new Person("段亚东",25); 12 //系统将per对象转换成字节序列输出 13 oos.writeObject(per); 14 //反序列化读取到的是ArrayList 15 ArrayList list = (ArrayList)ois.readObject(); 16 System.out.println(list); 17 } 18 catch(Exception ex) 19 { 20 ex.printStackTrace(); 21 } 22 23 } 24 25 }
上面程序使用writeObject()写入了一个Person对象,但第二行代码使用readObject()方法返回的实际上是一个ArrayList对象,这是因为Person类的writeReplace()方法返回了一个ArrayList对象,所以序列化机制在序列化Person对象时,实际上是转为序列化ArrayList对象。
根据上面的介绍,可以知道系统在序列化某个对象之前,会先调用该对象的writeReplace()和writeObject()两个方法,系统总是先调用被序列化对象的writeReplace()方法,如果该方法返回另一个对象,系统将再次调用另一个对象的writeReplace()方法,直到该方法不再返回另一个对象为止,程序最后将调用该对象的writeObject()方法来保存该对象的状态。