写在前面:月末惯例周六加班,不管怎样,今天至少有了一丝留下来的希望,希望顺顺利利,然后努力投入到工作中。
在平时的java应用中,经常需要将对象在网络上进行传送,或者将对象的字节序列保存在文件中,此时,就需要进行序列化。那么,今天,我们就复习下序列化相关的一些知识。
1.序列化和反序列化
概念:
序列化:把对象转换为字节序列的过程。
反序列化:把字节序列恢复为对象的过程。
序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象,序列化可以弥补不同操作系统之间的差异。
序列化应用场景:
(1) 永久性保存对象的字节序列到本地文件中;
(2) 通过序列化对象在网络中传递对象;
(3) 通过序列化在进程间传递对象。
如何实现序列化和反序列化:
对象所属的类必须实现Serializable或是Externalizable接口才能被序列化。
对实现了Serializable接口的类,其序列化与反序列化采用默认的序列化方式。
Externalizable接口是继承了Serializable接口的接口,是对Serializable的扩展,实现了Externalizable接口的类完全自己控制序列化与反序列化行为。
Java.io.ObjectOutputStream代表对象输出流,其方法writeObject(Object obj)可以实现对象的序列化,将得到的字节序列写到目标输出流中。
Java.io.ObjectInputStream代表对象输入流,其readObject()方法能从源输入流中读取字节序列,将其反序列化为对象,并将其返回。
实现序列化:
(1) 实现Serializable接口,未定义readObject()和writeObject()方法
ObjectOutputStream使用JDK默认方式对对象的非transient的实例变量进行序列化;
ObjectInputStream使用JDK默认方式对对象的非transient的实例变量进行反序列化。
(2) 实现Serializable接口,并且定义了readObject()和writeObject()方法
ObjectOutputStream调用类的writeObject(ObjectOutputStream out)方法对对象的非transient的实例变量进行序列化;
ObjectInputStream调用类的readObject(ObjectInputStream in)方法对对象的非transient的实例变量进行反序列化。
如果添加了这两个方法之后还想利用Java默认的序列化机制,则在这两个方法中分别调用defaultReadObject()和defaultWriteObject()两个方法。
(3) 实现ExternalSerializable接口,定义readExternal和writeExternal方法
ObjectOutputStream调用类的writeExternal方法对对象的非transient实例变量进行序列化;
ObjectInputStream首先通过类的无参数构造函数实例化一个对象,再用readExternal方法对对象的非transient实例变量进行反序列化。
注意事项:
(1) 被static修饰的属性不会被序列化 。
(2) 对象的类名、属性都会被序列化,方法不会被序列化 。
(3) 要保证序列化对象所在类的属性也是可以被序列化的。
(4) 当通过网络、文件进行序列化时,必须按照写入的顺序读取对象。
(5) 反序列化时必须有序列化对象时的class文件。
(6) 最好显示的声明serializableID,因为在不同的JVM之间,默认生成serializableID 可能不同,会造成反序列化失败。serializableID是static final long型。
(7) 对实现Externalizable接口来实现序列化的类而言,必须提供一个public的无参数构造函数,否则在反序列化时将出现异常。
2.序列化和反序列化步骤
序列化步骤:
步骤一:创建一个对象输出流,可以包装一个其它类型的目标输出流,如文件输出流。
步骤二:通过对象输出流的writeObject()方法写对象。
示例:
ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\\objectfile.obj”));
out.writeObject(“Hello”);
out.writeObject(new Date());
反序列化步骤:
步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流。
步骤二:通过对象输出流的readObject()方法读取对象。
示例:
ObjectInputStream in = new ObjectInputStream(new fileInputStream(“D:\\objectfile.obj”));
String obj1 = (String)in.readObject();
Date obj2 = (Date)in.readObject();
说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。
3.serialVersionUID的取值
serialVersionUID有两种用途:
(1) 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
(2) 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。
为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定义serialVersionUID,为它赋予明确的值。
4.transient关键字
作用:
一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
使用小结:
(1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
(2) transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
(3) 被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。
注意:反序列化后类中static型变量username的值为当前JVM中对应static变量的值,而不是序列化时的值。
被transient关键字修饰的变量真的不能被序列化吗?
在Java中,对象的序列化可以通过实现两种接口来实现,若实现的是Serializable接口,则所有的序列化将会自动进行,若实现的是Externalizable接口,则没有任何东西可以自动序列化,需要在writeExternal方法中进行手工指定所要序列化的变量,这与是否被transient修饰无关。
5.示例
定义一个学生Student类:
import java.io.Serializable;
/**
*Title:学生类
*Description:实现序列化接口的学生类
*/
public class Student implements Serializable{
private String name;
private char sex;
private int year;
private double gpa;
public Student(){
}
public Student(String name,char sex,int year,double gpa){
this.name = name;
this.sex = sex;
this.year = year;
this.gpa = gpa;
}
...getter and setter
}
实现类:
import java.io.*;
/**
*Title:应用学生类
*Description:实现学生类实例的序列化与反序列化
*/
public class UseStudent{
public static void main(String[] args){
Student st = new Student("Tom",'M',20,3.6);
File file = new File("d:\\Java\\com\\hw\\io\\student.txt");
try{
file.createNewFile();
}
catch(IOException e){
e.printStackTrace();
}
try{
//Student对象序列化过程
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(st);
oos.flush();
oos.close();
fos.close();
//Student对象反序列化过程
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Student st1 = (Student) ois.readObject();
System.out.println("name = " + st1.getName());
System.out.println("sex = " + st1.getSex());
System.out.println("year = " + st1.getYear());
System.out.println("gpa = " + st1.getGpa());
ois.close();
fis.close();
}
catch(ClassNotFoundException e){
e.printStackTrace();
}
catch (IOException e){
e.printStackTrace();
}
}
}
结果:
name = Tom
sex = M
year = 20
gpa = 3.6
6.不足和改进
如果采用默认的序列化方式,只要让一个类实现Serializable接口,其实例就可以被序列化。通常,专门为继承而设计的类应该尽量不要实现Serializable接口,因为一旦父类实现了Serializable接口,其所有子类也都是可序列化的了。
默认的序列化方式的不足之处:
(1) 直接对对象的不宜对外公开的敏感数据进行序列化,这是不安全的;
(2) 不会检查对象的成员变量是否符合正确的约束条件,有可能被篡改数据而导致运行异常;
(3) 需要对对象图做递归遍历,如果对象图很复杂,会消耗很多资源,设置引起Java虚拟机的堆栈溢出;
(4) 使类的接口被类的内部实现约束,制约类的升级与维护。
解决:
(1) 通过实现Serializable接口的private类型的writeObject()和readObject();
(2) 实现Externalizable接口,并实现writeExternal()与readExternal()方法,并提供public类型的无参数构造函数。
这两种方式控制序列化过程能够有效规避默认序列化方式的不足之处。