对象的序列化与反序列化
概念
对象的序列化:将Java对象转换为字节序列的过程称为对象的序列化。
对象的反序列化:把字节序列恢复为Java对象的过程称为对象的反序列化。
序列化的作用
1、把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中。
2、在网络上传送对象的字节序列。
实现序列化的方式
JDK类库中的序列化API,只有实现了Serializable或Externalizable接口的类对象才能被序列化,否则会出现java.io.NotSerializableException异常。
实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。
实现了Serializable接口的类A,按照如下方式序列化及反序列化A对象:
1、ObjectOutputStream 采用默认的序列化方式,A的对象的非transient的实例变量进行序列化。
2、ObjectInputStream采用默认的反序列化方式,A对象的非transient的实例变量进行反序列化。
实现了Serializable接口的类A,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputStream out)方法,按照如下方式序列化及反序列化A对象:
1、ObjectOutputStream会调用A类的writeObject(ObjectOutputStream out)方法进行序列化。
2、ObjectInputStream会调用A类的readObject(ObjectInputStream in)方法进行反序列化。
如果A类实现了Externalizable接口,那么A类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法。按照如下方式进行序列化与反序列化:
1、ObjectOutputStream会调用A类的writeExternal(ObjectOutput out)方法进行序列化。
2、ObjectInputStream会调用A类的readExternal(ObjectInput in)方法进行反序列化。
实现Serialiable接口
通过上面的分析,ObjectOutputStream只能对实现了Serializable接口的类的对象进行序列化。
代码示例
新建一个Custom
类实现Serializable
接口。并新建测试类,将序列化的数据保存到文件,然后再通过解析文件进行反序列化。
/**
* Customer类
*
* @author 在路上的coder
* @create 2017-03-06
**/
public class Customer implements java.io.Serializable {
private static final long serialVersionUID = -8763780870464864012L;
private static int count;
private static final int MAX_COUNT = 1000;
private String name;
private transient String password;
static {
System.out.println("调用了Customer类的静态代码块");
}
public Customer() {
System.out.println("调用了Customer类的不带参数的构造方法");
count++;
}
public Customer(String name, String password) {
System.out.println("调用了Customer类的带参数的构造方法");
this.name = name;
this.password = password;
count++;
}
public String toString() {
return "count=" + count + " MAX_COUNT=" + MAX_COUNT + " name=" + name + " password=" + password;
}
}
测试类
/**
* 测试类
*
* @author 在路上的coder
* @create 2017-03-07
**/
public class SerTest {
@Test
public void writeFile() throws IOException {
Customer customer = new Customer("张三", "12312");
System.out.println("序列化之前:" + customer);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("C:\\application\\JetBrains\\demo\\network\\src\\com\\knight\\chapt9\\objectFile.obj"));
objectOutputStream.writeObject(customer);
objectOutputStream.close();
}
@Test
public void readFile() throws IOException, ClassNotFoundException {
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("C:\\application\\JetBrains\\demo\\network\\src\\com\\knight\\chapt9\\objectFile.obj"));
Customer serCustomer = (Customer) inputStream.readObject();
System.out.println("序列化之后:"+serCustomer);
inputStream.close();
}
}
执行writeFile()
方法输出如下:
执行readFile()
方法输出如下:
结论:
从上面打印的结果可以看到,反序列化的时候,加载并初始化Customer类,在初始化时,
- 先执行静态代码块。
- 并将静态变量count的值初始化为0。
- 反序列化时不会调用类的任何构造方法。
- 被transient关键字修饰的没有被序列化。
序列化对象图
类与类之间可能存在关联关系。Customer与Order之间存在一对多的关系。
添加Order类的代码:
/**
* 订单类
* @author 在路上的coder
* @create 2017-03-07
**/
public class Order implements java.io.Serializable{
private static final long serialVersionUID = 7027615442146635452L;
private String orderId;//订单号
private BigDecimal total;//订单金额
public Order(String orderId,BigDecimal total){
this.orderId = orderId;
this.total = total;
}
@Override
public String toString() {
return "Order{" +
"orderId='" + orderId + '\'' +
", total=" + total +
'}';
}
}
在序列化Customer类的时候Order类也会序列化。
如下图,按照默认方式序列化对象A时,将会序列化那些对象?
在实例A对象的时候,将会实例化B、C、D、E对象。
控制序列化的行为
实现方式:
通过在可序列化类中提供以下方法:
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {}
private void writeObject(ObjectOutputStream outputStream) throws IOException {}
当ObjectOutputStream对一个Customer对象进行序列化时,如果Customer对象具有writeObject()方法,那么就会执行这一方法,否则就按默认方式序列化,反序列化也是如此。
下面通过两个例子来验证这一点,其中一个示例是,对敏感数据序列化时加密,反序列化时解密。
/**
* Customer类
*
* @author 在路上的coder
* @create 2017-03-06
**/
public class Customer implements java.io.Serializable {
private static final long serialVersionUID = -8763780870464864012L;
private static int count;
private static final int MAX_COUNT = 1000;
private String name;
private transient String password;
static {
System.out.println("调用了Customer类的静态代码块");
}
public Customer() {
System.out.println("调用了Customer类的不带参数的构造方法");
count++;
}
public Customer(String name, String password) {
System.out.println("调用了Customer类的带参数的构造方法");
this.name = name;
this.password = password;
count++;
}
public String toString() {
return "count=" + count + " MAX_COUNT=" + MAX_COUNT +
" name=" + name + " password=" + password;
}
/**
* 加密数组
*
* @param buff
* @return
*/
private byte[] change(byte[] buff) {
for (int i = 0; i < buff.length; i++) {
int b = 0;
for (int j = 0; j < 8; j++) {
int bit = (buff[i] >> j & 1) == 0 ? 1 : 0;
b += (1 << j) * bit;
}
}
return buff;
}
/**
* 反序列化执行方法
*
* @param inputStream
* @throws IOException
* @throws ClassNotFoundException
*/
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
System.out.println("开始执行反序列化方法");
inputStream.defaultReadObject();
byte[] bytes = (byte[]) inputStream.readObject();
password = new String(change(bytes));
}
/**
* 序列化执行方法
*
* @param outputStream
* @throws IOException
*/
private void writeObject(ObjectOutputStream outputStream) throws IOException {
System.out.println("开始执行序列化方法");
outputStream.defaultWriteObject();
outputStream.writeObject(change(password.getBytes()));//序列化密码
}
}
对象属性有效性校验示例,新建一个Student类,该类有一个age属性,该属性的值必须大于0;
/**
* 测试序列化校验有效性
* @author 在路上的coder
* @create 2017-03-07
**/
public class Student implements java.io.Serializable{
private static final long serialVersionUID = 35245843321025025L;
private int age;
public Student(int age){
if(age<0){
throw new IllegalArgumentException("年龄必须大于零");
}
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
}
测试方法:
@Test
public void testValidate() throws IOException, ClassNotFoundException {
Student student = new Student(25);
System.out.println("序列化之前:"+student);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
//将Custom4对象序列化到一个字节缓存中
ObjectOutputStream objectOutputStream = new ObjectOutputStream(buf);
objectOutputStream.writeObject(student);
byte[] byteArray = buf.toByteArray();
for(int i=0;i<byteArray.length;i++){
System.out.print(byteArray[i]+" ");
if((i%10==0 && i!=0) || i==byteArray.length-1){
System.out.println();
}
}
//篡改序列化数据
byteArray[byteArray.length-4] = -1;
byteArray[byteArray.length-3] = -1;
byteArray[byteArray.length-2] = -1;
byteArray[byteArray.length-1] = -10;
//从字节缓冲中反序列化Customer4对象
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(byteArray));
Student s = (Student) inputStream.readObject();
System.out.println("序列化之后:"+s);
}
执行结果:
类的age属性已经被修改了,而且还无法校验。通过添加readObject()方法来实现校验。修改Student类如下:
/**
* 测试序列化校验有效性
* @author 在路上的coder
* @create 2017-03-07
**/
public class Student implements java.io.Serializable{
private static final long serialVersionUID = 35245843321025025L;
private int age;
public Student(int age){
if(age<0){
throw new IllegalArgumentException("年龄必须大于零");
}
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
if(age<0){
throw new IllegalArgumentException("年龄必须大于零");
}
}
}
再次执行测试方法,结果如下图:
readResolve()方法在单例类中的应用
单例类是指只有一个实例的类。无论采用默认方式,还是采用用户自定义的方式,反序列化都会创建一个新的对象。
代码如下:
/**
* 单例模式
* @author 在路上的coder
* @create 2017-03-07
**/
public class Singleton implements java.io.Serializable{
private static final Singleton INSTANCE = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton singleton = Singleton.getInstance();
ByteArrayOutputStream buff = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(buff);
outputStream.writeObject(singleton);
ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(buff.toByteArray()));
Singleton newSingleton= (Singleton) inputStream.readObject();
System.out.println("singleton==newSingleton:"+(singleton==newSingleton));
System.out.println("singleton.equals(newSingleton):"+(singleton.equals(newSingleton)));
}
}
执行结果如下:
由此可见,反序列化打破了单例类只能有一个实例的约定。为了避免这一问题,可以在 Singleton类中增加一个readResolve()方法。
public Object readResolve(){
return INSTANCE;
}
执行结果如下:
执行流程如下:
如果一个类提供了readResolve()方法,那么在执行反序列化操作时,先按照默认方式或者自定义的方式进行反序列化,最后调用readResolve()方法,该方法返回的对象为反序列化的最终结果。
有readResolve()方法那自然就有writeReplace()方法,用来重新指定被序列化的对象。writeReplace()方法返回一个Object类型的对象,这个返回对象才是真正要被序列化的对象。
实现Externalizable接口
Externalizable接口继承自Serializable接口,如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为。Externalizable接口中申明了两个方法:
void readExternal(ObjectInput in)throws IOException, ClassNotFoundException;
void writeExternal(ObjectOutput out)throws IOException
writeExternal()负责序列化操作,readExternal()方法负责反序列化操作。在实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法,这里有别于实现Serializable默认反序列化方式。
可序列化类的不同版本
凡是实现了Serializable接口的类都有一个表示序列化版本标识符的静态常量:
private static final long serialVersionUID = 35245843321025025L;
以上serialVersionUID 的取值是Java运行时环境根据类的内部细节自动生成的。如果不指定的话,如果对类的源码进行修改,再重新编译,新生成的文件的serialVersionUID的取值也会发生变化。
类的serialVersionUID的默认值依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列类中显示地定义serialVersionUID,为它赋予明确的值。显示定义serialVersionUID的用途:
1、在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID。
2、在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
Tips
是要serialVersionUID来控制序列化兼容性的能力非常有限,就算serialVersionUID相通,任然可能出现序列化不兼容的情况。因为序列化兼容性不仅取决于serialVersionUID,还取决于类的不同版本的实现细节和序列化细节。
总结
本文介绍了实现序列化的方式、控制序列化的行为、并使单例类实现真正的单例还有序列化后类的版本兼容问题。
欢迎关注微信公众号 在路上的coder
每天分享优秀的Java技术文章,还有学习视频分享!
扫描二维码关注: