提到Java序列化,相信大家都不陌生。我们在序列化的时候,需要将被序列化的类实现Serializable接口,这样的类在序列化时,会默认将所有的字段都序列化。那么当我们在序列化Java对象时,如果不希望对象中某些字段被序列化(如密码字段),怎么实现呢?看一个例子:
1import java.io.Serializable; 2import java.util.Date; 3 4public class LoginInfo implements Serializable { 5 private static final long serialVersionUID = 8364988832581114038L; 6 private String userName; 7 private transient String password;//Note this key word "transient" 8 private Date loginDate; 9 10 //Default Public Constructor 11 public LoginInfo() { 12 System.out.println("LoginInfo Constructor"); 13 } 14 15 //Non-Default constructor 16 public LoginInfo(String username, String password) { 17 this.userName = username; 18 this.password = password; 19 loginDate = new Date(); 20 } 21 public String toString() { 22 return "UserName=" + userName + ", Password=" 23 + password + ", LoginDate=" + loginDate; 24 } 25}
测试类:
1import java.io.FileInputStream; 2import java.io.FileOutputStream; 3import java.io.ObjectInputStream; 4import java.io.ObjectOutputStream; 5 6public class Test { 7 static String fileName = "C:/x.file"; 8 public static void main(String[] args) throws Exception { 9 LoginInfo info = new LoginInfo("name", "123"); 10 System.out.println(info); 11 //Write 12 System.out.println("Serialize object"); 13 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName)); 14 oos.writeObject(info); 15 oos.close(); 16 //Read 17 System.out.println("Deserialize object."); 18 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); 19 LoginInfo info2 = (LoginInfo)ois.readObject(); 20 ois.close(); 21 System.out.println(info2); 22 } 23}
执行结果:
1UserName=name, Password=123, LoginDate=Wed Nov 04 16:41:49 CST 2015 2Serialize object 3Deserialize object. 4UserName=name, Password=null, LoginDate=Wed Nov 04 16:41:49 CST 2015
另一种可以达到此目的的方法可能就比较少用了,那就是——不实现Serializable而实现Externalizable接口。这个Externalizable接口有两个方法,分别表示在序列化的时候需要序列化哪些字段和反序列化的时候能够反序列化哪些字段:
1void writeExternal(ObjectOutput out) throws IOException; 2void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
于是就有了下面的代码:
1import java.io.Externalizable; 2import java.io.IOException; 3import java.io.ObjectInput; 4import java.io.ObjectOutput; 5import java.util.Date; 6 7public class LoginInfo2 implements Externalizable { 8 private static final long serialVersionUID = 8364988832581114038L; 9 private String userName; 10 private String password; 11 private Date loginDate; 12 13 //Default Public Constructor 14 public LoginInfo2() { 15 System.out.println("LoginInfo Constructor"); 16 } 17 18 //Non-Default constructor 19 public LoginInfo2(String username, String password) { 20 this.userName = username; 21 this.password = password; 22 loginDate = new Date(); 23 } 24 public String toString() { 25 return "UserName=" + userName + ", Password=" 26 + password + ", LoginDate=" + loginDate; 27 } 28 29 @Override 30 public void writeExternal(ObjectOutput out) throws IOException { 31 System.out.println("Externalizable.writeExternal(ObjectOutput out) is called"); 32 out.writeObject(loginDate); 33 out.writeUTF(userName); 34 } 35 36 @Override 37 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 38 System.out.println("Externalizable.readExternal(ObjectInput in) is called"); 39 loginDate = (Date)in.readObject(); 40 userName = (String)in.readUTF(); 41 } 42}
测试类除了类名使用LoginInfo2以外,其他保持不变。下面是执行结果:
1UserName=name, Password=123, LoginDate=Wed Nov 04 16:36:39 CST 2015 2Serialize object 3Externalizable.writeExternal(ObjectOutput out) is called 4Deserialize object. 5LoginInfo Constructor //-------------------------Note this line 6Externalizable.readExternal(ObjectInput in) is called 7UserName=name, Password=null, LoginDate=Wed Nov 04 16:36:39 CST 2015
可以看到,反序列化后的Password一项依然为null。
需要注意的是:对于恢复Serializable对象,对象完全以它存储的二进制为基础来构造,而不调用构造器。而对于一个Externalizable对象,public的无参构造器将会被调用(因此你可以看到上面的测试结果中有LoginInfoConstructor这一行),之后再调用readExternal()方法。在Externalizable接口文档中,也给出了相关描述:
When an Externalizable object is reconstructed, an instance is created using the public no-arg constructor, then the readExternal method called. Serializable objects are restored by reading them from an ObjectInputStream.
如果没有发现public的无参构造器,那么将会报错。(把LoginInfo2类的无参构造器注释掉,就会产生错误了):
1UserName=name, Password=123, LoginDate=Wed Nov 04 17:03:24 CST 2015 2Serialize object 3Deserialize object. 4Exception in thread "main" java.io.InvalidClassException: LoginInfo2; no valid constructor 5 at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150) 6 at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768) 7 at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772) 8 at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) 9 at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370) 10 at Test2.main(Test2.java:19)
那么,如果把Externalizable接口和transient关键字一起用,会是什么效果呢?我们在LoginInfo2中的password加上关键字transient,再修改writeExternal()和readExternal()方法:
1 @Override 2 public void writeExternal(ObjectOutput out) throws IOException { 3 out.writeObject(loginDate); 4 out.writeUTF(userName); 5 out.writeUTF(password);//强行将transient修饰的password属性也序列化进去 6 } 7 8 @Override 9 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 10 loginDate = (Date)in.readObject(); 11 userName = (String)in.readUTF(); 12 password = (String)in.readUTF();//反序列化password字段 13 }
执行结果:
1UserName=name, Password=123, LoginDate=Wed Nov 04 16:58:27 CST 2015 2Serialize object 3Deserialize object. 4LoginInfo Constructor 5UserName=name, Password=123, LoginDate=Wed Nov 04 16:58:27 CST 2015
从结果中可以看到,尽管在password字段上使用了transient关键字,但是这还是没能阻止被序列化。因为不是以Serializable方式去序列化和反序列化的。也就是说:transient关键字只能与Serializable接口搭配使用。