在一些特殊的场景下,如果一个类里包含的某些实例变量是敏感信息,例如银行账户信息,这时不希望系统将该实例变量值进行实例化;或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归实例化,以避免引发异常。
------- android培训、java培训、期待与您交流! ----------
通过在实例变量前面使用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()方法来保存该对象的状态。
与writeReplace()方法相对应的是,序列化机制里还有一个特殊的方法,它可以实现保护性复制整个对象,这个方法就是
Object readResolve()
这个方法会紧挨着readObject()之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象将会立即丢弃。
readObject()方法在序列化单例类,枚举类时尤其有用。当然,如果使用java5提供的enum来定义枚举类,则完全不用担心,程序没有任何问题,但如果应用中有早期遗留下来的枚举类,例如下面的Orientation类就是一个枚举类。
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;
}
}
在java5以前,这种代码是很常见的。Orientation类的构造私有,程序只有两个Orientation对象,分别通过Orientation的两个常量来引用。但如果让该类实现Serializable接口,则会引发一个问题,如果将一个Orientation.HORIZONTALZ值序列化后再读出,如下代码所示:
1 public class OrientationDemo 2 { 3 public static void main(String[] args) throws Exception 4 { 5 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("transient.txt")); 6 7 //写入Orientation.HORIZONTAL值 8 oos.writeObject(Orientation.HORIZONTAL); 9 10 //创建一个ObjectInputStream输入流 11 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("transient.txt")); 12 13 //读取刚刚序列化的值 14 Orientation ori = (Orientation) ois.readObject(); 15 16 System.out.println(ori == Orientation.HORIZONTAL);//false 17 } 18 19 }
两个值比较,返回falase,也就是说,ori是一个新的Orientation对象,而不等于Orientation类中的任何枚举值,虽然Orientation的构造器是私有的,但反序列化依然可以创建Orientation对象。
反序列化机制在恢复java对象时无须调用构造器来初始化java对象。从这个意义上来看,序列化机制可以用来"克隆"对象。
在这种情况下,可以通过为Orientation类提供一个readResolve()方法来解决该问题,readResolve()方法的返回值将会代替原来反序列化的对象,也就是让反序列化得到的Orientation对象被直接丢弃。下面是为Orientation类提供的readResolve()方法。
private Object readResolve() throws ObjectStreamException { if(value == 1) { return HORIZONTAL; } if(value == 2) { return VERTICAL; } return null; }
通过重写readResolve()方法可以保证反序列化得到的依然是Orientation的HORIZOHTAL或VERTICAL两个枚举值之一。
所有单例类,枚举类在实现序列化时都应该提供readResolve()方法,这样才可以保证反序列化的对象依然正常。
与readReplace()方法类似的是,readResolve()方法也可以使用任意的访问控制符,因此父类的readResolve()方法可能被其子类继承。这样利用readResolve()方法时就会存在一个明显的缺点,就是当父类已经实现了readResolve()方法后,子类将变得无从下手。如果父类包含一个protected或public的readResolve()方法,而且子类也没有重写该方法,将会使得子类反序列化时得到一个父类的对象,这显然不是程序要的结果,而且也不容易发现这种错误。总是让子类重写readResolve()方法无疑是一个负担,因此对于要被作为父类继承的类而言,实现readResolve()方法可能有一些潜在的危险。
通常的建议是,对于final类重写readResolve()方法不会有任何问题:否则,重写readResolve()方法时应尽量使用private修饰该方法。
另一种自定义序列化机制
java还提供了另一种序列化机制,这种序列化方式完全由程序员决定存储和恢复对象数据。要实现该目标,java类必须实现Externalizable接口,该接口里定义了如下两个方法。
void readExternal(ObjectInput in): 需要序列化的类实现 该方法来实现反序列化。该方法调用DataInput(它是ObjectInput 的父接口) 的方法来恢复基本类型的实例变量值,调用ObjectInput的readObject()方法来恢复引用类型的实例变量值。
void writeExternal(Object out): 需要序列化的类实现该方法来保存对象的状态。该方法调用DataOutput(它是ObjectOutput 的父接口)的方法来保存基本类型的实例变量值,调用ObjectOutput的writeObject()方法来保存引用类型的实例变量值。
实际上,采用实现Externalizable接口方式的序列化与前面介绍的自定义序列化非常相似,只是Externalizable接口强制自定义序列化。下面的Person类实现了Externalizable接口,并且实现了该接口里提供的两个方法,用以实现自定义序列化。
1 public class Person implements Externalizable 2 { 3 private String name; 4 private int age; 5 //此处必须提供无参构造,否则反序列化时会失效 6 public Person(){} 7 public Person(String name, int age) 8 { 9 System.out.println("有参数的构造器"); 10 this.name = name; 11 this.age = age; 12 } 13 14 15 public String getName() { 16 return name; 17 } 18 public void setName(String name) { 19 this.name = name; 20 } 21 public int getAge() { 22 return age; 23 } 24 public void setAge(int age) { 25 this.age = age; 26 } 27 28 @Override 29 public void writeExternal(java.io.ObjectOutput out) throws IOException 30 { 31 //将name实例变量值反转后写入二进制流 32 out.writeObject(new StringBuffer(name).reverse()); 33 out.writeInt(age); 34 35 } 36 37 @Override 38 public void readExternal(java.io.ObjectInput in) throws IOException, ClassNotFoundException 39 { 40 ------- android培训、java培训、期待与您交流! ---------- #008000;">//将读取的字符串反转后赋给name实例变量
41 this.name = ((StringBuffer)in.readObject()).reverse().toString(); 42 this.age = in.readInt(); 43 44 } 45 46 }
上面程序中Person类实现了Externalizable接口,该Person类还实现了readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。
如果程序需要序列化实现Externalizable接口的对象,一样调用ObjectOutputStream的writeObject()方法输出该对象即可;反序列化该对象,则调用ObjectInputStream的readObject()方法。
需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。
虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。