解决transient关键字的作用之前,我们需要了解java的序列化和反序列化。因为transient涉及到了这两点。
序列化:我们通俗点理解就是jvm能够我们创建的对象保存为字符数组。
通常在java中,我们可以通过使用new关键字、使用Class类的newInstance方法、使用Constructor类的newInstence方法、使用Clone方法、或者使用反序列化的方法创建对象,并且只要对象没有被销毁回收,我们还是能够复用该对象。但是,我们创建的java对象都存在JVM的堆内存中,只有JVM处于运行的状态的时候,我们才能使用这些对象,而一旦JVM停止运行,这些对象的状态也随之而丢失。因此,在许多的真实场景中,我们需要将这些对象持久化下来,并且能够在需要的时候,将对象重新读取出来。而java的对象序列化可以帮助我们实现这个需求。
对象序列化机制(object serialization)是java语言的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个数组通过反序列化的方式在转化为对象,对象序列化可以很容易的在jvm中的活动对象和字节数组(流)之间进行转换。
实现java对象序列化的方式方法
1.java.io.Serializable接口
类通过实现 java.io.Serializable
接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。 (该接口并没有方法和字段,为什么只有实现了该接口的类的对象才能被序列化呢?)
当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出NotSerializableException
。
如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该集成java.io.Serializable
接口。
public class LoggingInfo implements java.io.Serializable{
private static final long serialVersionUID = 1L;
private Date loggingDate = new Date();
private String uid;
private String pwd;
LoggingInfo(String user, String password){
uid = user;
pwd = password;
}
public String toString(){
String password=null;
if(pwd == null){
password = "NOT SET";
}else{
password = pwd;
}
return "logon info: \n " + "user: " + uid +
"\n logging date : " + loggingDate.toString() +
"\n password: " + password;
}
}
LoggingInfo logInfo = new LoggingInfo("MIKE", "MECHANICS");
System.out.println(logInfo.toString());
try{
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("logInfo.out"));
o.writeObject(logInfo);
o.close();
}catch(Exception e){}
try{
ObjectInputStream in =new ObjectInputStream( new FileInputStream("logInfo.out"));
LoggingInfo a = (LoggingInfo)in.readObject();
System.out.println(a.toString());
} catch(Exception e) {}
我们会发现控制台打印如下:
这里两次的打印是一样的!这里我们将LoggingInfo对象先存储到内存中,然后再读取出来,结果是一致的!对象持久化了!
2.java.io.Externalizable接口
Externalizable接口和Serializable类似。为了区分两者的区别,我们将上面的代码改为Externalizable。
public class LoggingInfo implements java.io.Externalizable{
private Date loggingDate = new Date();
private String uid;
private String pwd;
// LoggingInfo(){
// System.out.println("使用了默认的构造函数");
// }
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
public String toString(){
String password=null;
if(pwd == null){
password = "NOT SET";
}else{
password = pwd;
}
return "logon info: \n " + "user: " + uid +
"\n logging date : " + loggingDate.toString() +
"\n password: " + password;
}
/* (non-Javadoc)
* @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
*/
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
}
/* (non-Javadoc)
* @see java.io.Externalizable#readExternal(java.io.ObjectInput)
*/
public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
// TODO Auto-generated method stub
}
}
LoggingInfo logInfo = new LoggingInfo();
logInfo.setUid("MIKE");
logInfo.setPwd("MECHANICS");
System.out.println(logInfo.toString());
try{
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("logInfo.out"));
o.writeObject(logInfo);
o.close();
}catch(Exception e){}
//Read Obj from File
File file = new File("logInfo.out");
ObjectInputStream in = null;
try{
in =new ObjectInputStream( new FileInputStream(file));
LoggingInfo a = (LoggingInfo)in.readObject();
System.out.println(a.toString());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
这里我们发现,对象没有持久化。都为空
通过上面的实例可以发现,对User1类进行序列化及反序列化之后得到的对象的所有属性的值都变成了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。这就是Externalizable接口和Serializable接口的区别:
Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()
与readExternal()
。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与readExternal()
方法。由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。
修改代码后,主要是重写这两个方法
/* (non-Javadoc)
* @see java.io.Externalizable#writeExternal(java.io.ObjectOutput)
*/
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(uid);
out.writeObject(pwd);
}
/* (non-Javadoc)
* @see java.io.Externalizable#readExternal(java.io.ObjectInput)
*/
public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException {
uid = (String) in.readObject();
pwd = (String) in.readObject();
}
3.
ObjectOutput和ObjectInput 接口
ObjectInput接口 扩展自 DataInput 接口以包含对象的读操作。
DataInput 接口用于从二进制流中读取字节,并根据所有 Java 基本类型数据进行重构。同时还提供根据 UTF-8 修改版格式的数据重构 String 的工具。
对于此接口中的所有数据读取例程来说,如果在读取所需字节数之前已经到达文件末尾 (end of file),则将抛出 EOFException(IOException 的一种)。如果因为到达文件末尾以外的其他原因无法读取字节,则将抛出 IOException 而不是 EOFException。尤其是,在输入流已关闭的情况下,将抛出 IOException。
ObjectOutput 扩展 DataOutput 接口以包含对象的写入操作。
DataOutput 接口用于将数据从任意 Java 基本类型转换为一系列字节,并将这些字节写入二进制流。同时还提供了一个将 String 转换成 UTF-8 修改版格式并写入所得到的系列字节的工具。
对于此接口中写入字节的所有方法,如果由于某种原因无法写入某个字节,则抛出 IOException。
ObjectOutputStream类和ObjectInputStream类
通过前面的代码片段中我们也能知道,我们一般使用ObjectOutputStream的writeObject
方法把一个对象进行持久化。再使用ObjectInputStream的readObject
从持久化存储中把对象读取出来。
更多关于ObjectInputStream和ObjectOutputStream的相关知识欢迎阅读:深入分析Java的序列化与反序列化、单例与序列化的那些事儿
Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。关于Transient 关键字的拓展知识欢迎阅读深入分析Java的序列化与反序列化
public class LoggingInfo implements java.io.Serializable{
private static final long serialVersionUID = 1L;
private Date loggingDate = new Date();
private String uid;
private transient String pwd;
LoggingInfo(String user, String password){
uid = user;
pwd = password;
}
public String toString(){
String password=null;
if(pwd == null){
password = "NOT SET";
}else{
password = pwd;
}
return "logon info: \n " + "user: " + uid +
"\n logging date : " + loggingDate.toString() +
"\n password: " + password;
}
}
LoggingInfo logInfo = new LoggingInfo("MIKE", "MECHANICS");
System.out.println(logInfo.toString());
try{
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("logInfo.out"));
o.writeObject(logInfo);
o.close();
}catch(Exception e){}
try{
ObjectInputStream in =new ObjectInputStream( new FileInputStream("logInfo.out"));
LoggingInfo a = (LoggingInfo)in.readObject();
System.out.println(a.toString());
} catch(Exception e) {}
会发现 password变为not set 了。表示未序列化。
5.序列化ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID
)
序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。