提到Java序列化,相信大家都不陌生。我们在序列化的时候,需要将被序列化的类实现Serializable接口,这样的类在序列化时,会默认将所有的字段都序列化。那么当我们在序列化Java对象时,如果不希望对象中某些字段被序列化(如密码字段),怎么实现呢?看一个例子:
import java.io.Serializable; import java.util.Date; public class LoginInfo implements Serializable { private static final long serialVersionUID = 8364988832581114038L; private String userName; private transient String password;//Note this key word "transient" private Date loginDate; //Default Public Constructor public LoginInfo() { System.out.println("LoginInfo Constructor"); } //Non-Default constructor public LoginInfo(String username, String password) { this.userName = username; this.password = password; loginDate = new Date(); } public String toString() { return "UserName=" + userName + ", Password=" + password + ", LoginDate=" + loginDate; } }
测试类:
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Test { static String fileName = "C:/x.file"; public static void main(String[] args) throws Exception { LoginInfo info = new LoginInfo("name", "123"); System.out.println(info); //Write System.out.println("Serialize object"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName)); oos.writeObject(info); oos.close(); //Read System.out.println("Deserialize object."); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName)); LoginInfo info2 = (LoginInfo)ois.readObject(); ois.close(); System.out.println(info2); } }
执行结果:
UserName=name, Password=123, LoginDate=Wed Nov 04 16:41:49 CST 2015 Serialize object Deserialize object. UserName=name, Password=null, LoginDate=Wed Nov 04 16:41:49 CST 2015
另一种可以达到此目的的方法可能就比较少用了,那就是——不实现Serializable而实现Externalizable接口。这个Externalizable接口有两个方法,分别表示在序列化的时候需要序列化哪些字段和反序列化的时候能够反序列化哪些字段:
void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
于是就有了下面的代码:
import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.Date; public class LoginInfo2 implements Externalizable { private static final long serialVersionUID = 8364988832581114038L; private String userName; private String password; private Date loginDate; //Default Public Constructor public LoginInfo2() { System.out.println("LoginInfo Constructor"); } //Non-Default constructor public LoginInfo2(String username, String password) { this.userName = username; this.password = password; loginDate = new Date(); } public String toString() { return "UserName=" + userName + ", Password=" + password + ", LoginDate=" + loginDate; } @Override public void writeExternal(ObjectOutput out) throws IOException { System.out.println("Externalizable.writeExternal(ObjectOutput out) is called"); out.writeObject(loginDate); out.writeUTF(userName); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { System.out.println("Externalizable.readExternal(ObjectInput in) is called"); loginDate = (Date)in.readObject(); userName = (String)in.readUTF(); } }
测试类除了类名使用LoginInfo2以外,其他保持不变。下面是执行结果:
UserName=name, Password=123, LoginDate=Wed Nov 04 16:36:39 CST 2015 Serialize object Externalizable.writeExternal(ObjectOutput out) is called Deserialize object. LoginInfo Constructor //-------------------------Note this line Externalizable.readExternal(ObjectInput in) is called UserName=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类的无参构造器注释掉,就会产生错误了):
UserName=name, Password=123, LoginDate=Wed Nov 04 17:03:24 CST 2015 Serialize object Deserialize object. Exception in thread "main" java.io.InvalidClassException: LoginInfo2; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370) at Test2.main(Test2.java:19)
那么,如果把Externalizable接口和transient关键字一起用,会是什么效果呢?我们在LoginInfo2中的password加上关键字transient,再修改writeExternal()和readExternal()方法:
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(loginDate); out.writeUTF(userName); out.writeUTF(password);//强行将transient修饰的password属性也序列化进去 } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { loginDate = (Date)in.readObject(); userName = (String)in.readUTF(); password = (String)in.readUTF();//反序列化password字段 }
执行结果:
UserName=name, Password=123, LoginDate=Wed Nov 04 16:58:27 CST 2015 Serialize object Deserialize object. LoginInfo Constructor UserName=name, Password=123, LoginDate=Wed Nov 04 16:58:27 CST 2015
从结果中可以看到,尽管在password字段上使用了transient关键字,但是这还是没能阻止被序列化。因为不是以Serializable方式去序列化和反序列化的。也就是说:transient关键字只能与Serializable接口搭配使用。