Java之序列化和反序列化
题记
在谈序列化和反序列化之前,我想先说两点:一点是我的学习方法,每学一个具体的技术,我们都要追根溯源,明白这门技术是什么?为什么会产生这门技术?这门技术有什么用?又怎么用?我想弄明白这几个问题,你对这门技术也算初窥门径了,剩下的更多是去用代码来实践了。
第二点是我最近博客的侧重点会偏向于我不是特别熟悉的领域,比如这次的话题,以及克隆,反射等内容,之前公共语言基础的内容我会在后续补上。
什么是序列化和反序列化?
序列化和反序列化是Java的IO体系中的一块内容,这里简单解释一下IO,IO就是一组对数据进行输入输出的流,这个概念我会在之后的博客中提及。
它产生的原因是因为想要使对象能够在程序不运行的情况下仍能存在并保存其信息,这样在下次运行程序时,该对象将被重建并且拥有的信息,与在程序上次运行时它所拥有的信息相同。
那如何实现该功能呢?那肯定是通过序列化和反序列化。所以,序列化就是将对象通过输出流输出为二进制的字节序列,反序列化就是将二进制的字节序列通过输入流转换为对象。这样就可以实现轻量级持久性。
“持久性”意味着一个对象的生命周期并不取决于程序是否正在执行,它可以生存于程序的调用之间。“轻量级”是指因为不能用某种“persistent”(持久)关键字来简单定义一个对象,并让系统自动维护其他细节问题。
序列化和反序列化的作用
对象序列化的概念加入到java语言中主要是为了支持两种主要特性:
第一种是Java的远程方法调用(RMI):它使存活于其他计算机上的对象使用起来就像存活于本机一样。当向远程对象发送消息时,需要通过对象序列化来传输参数和返回值。
第二种是序列化Java Beans对象:使用一个Bean对象,一般情况是在设计阶段对它的状态信息进行配置,这种状态信息必须保存下来,并在程序启动时进行后期恢复。
那什么是Java Beans呢,Java Beans只是一个命名规则:
- 对XXX属性有与之对应的get和set方法,如果该属性是boolean类型的,把get替换为is。
- Bean的普通方法不必遵循以上的命名规则,但它必须是public的。
- 对应事件,要使用Swing中处理监听器的方式。比如使用addBounceListener(BounceListener)和removeBounceListener(BounceListener)用来处理BounceEvent事件。
说白了,我们常用的实体类就是一个Java Beans。
同时,序列化也可以用来实现对象的深拷贝(深克隆)。因为序列化不仅可以保存对象的“全景图”,而且能追踪到对象内部所包含的所有引用,并保存那些对象,接着又能对对象内包含的每个这样的引用进行追踪,以此类推。这种情况有时被称为**“对象网”**。关于克隆的知识未来会提到。
如何具体的使用序列化和反序列化呢?
(1)使用Serializable接口
一个对象想要被序列化,就需要实现Serializable接口,该接口仅是一个标记接口,其中没有任何方法和属性。
其次,只需要将OutputStream对象封装在ObjectOutputStream对象中,调用writeObject()即可将之序列化。反之,使用相应的input流,调用readObject()方法就可以反序列化。
值得一提的是,对于通过Serializable接口实现序列化的对象来说,对象完全以它存储的二进制位为基础来构造,而不调用构造器。
(2)使用Externalizable接口
序列化同时面临一个不安全的问题,即一个属性通过private修饰,一经序列化,人们就可以通过读取文件或拦截网络传输的方式获取它。如果你不希望对象的某一部分被序列化,那该怎么办呢?
那就是使需要被序列化的对象实现Externalizable接口,来替代Serializable接口,进而对序列化过程进行控制。
该Externalizable接口继承了Serializable接口,同时增添了两个方法:writeExternal()写入重要信息,和readExternal()恢复数据。这两个方法会在序列化和反序列化还原的过程中被自动调用。
同时,对于通过Externalizable接口实现序列化的对象,会通过其默认构造方法来实例化对象的,然后才会执行readExteernal()方法,所有默认构造方法必须是public修饰的,不然会造成异常,无法正常反序列化。
那么,Serializable接口就不可以控制对象的序列化么?答案是当然可以。
如何实现Serializable对序列化的控制
(1) transient(瞬时)关键字
使用transient关键字可以逐个字段的关闭序列化,说白了就是可以使被修饰的属性不参与序列化。所有它仅能修饰属性,不能修饰方法和类。并且只能和Serializable一起使用。
同时,需要注意的是,静态属性不是对象的状态,所以它是不参与序列化的,将静态属性上修饰和不修饰transient关键字都是一样不被序列化的。
而通过final修饰的属性是通过值参与序列化的,所以无论通不通过transient修饰常量,该值都是要参与序列化的。
(2)添加writeObject()和readObject()方法
在这里,需要在类中定义具有准确方法签名的这两个方法,来实现对序列化的控制。
具体的方法签名:
private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectOutputStream stream) throws IOException,ClassNotFoundException;
这个类不是基类或者Serializable接口的一部分,所以应该在自己的接口中定义。同时,它是private,意味着它仅能被这个类的其他成员调用,但实际上,确是被ObjectOutputStream对象中weiteObject()方法和输入流中的读方法调用(或许是通过反射?还未读源码)。
serialVersionUID版本控制
如果你把A对象通过序列化生成的对象的A文件,此时,如果你将A对象的某一个属性进行修改,那么,A文件还可以被反序列化回来么?这个被反序列化回来的对象还是我们需要的么?
如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID。但强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException异常。
所以,当你对A对象进行修改,其serialVersionUID也会被修改。而字节序列中的serialVersionUID必须和对象中的serialVersionUID一致,才可以被反序列化。所以通过serialVersionUID来实现序列化的版本一致。
需要注意的是,serialVersionUID是long类型的,且被private、static、final修饰。
那么,如何自动生成serialVersionUID呢?请参考下面的帖子。
如何生成serialVersionUID(作者: godtrue )
具体代码实现
说了这么多,那如何通过Java代码来实现呢?
1.首先我们创建一个可被序列化的对象:
package com.ztc.serializable;
import java.io.Serializable;
public class JavaBeansForUser implements Serializable {
private static final long serialVersionUID = 6351505220758788992L;
private String username;
private transient String password;//我们不想要密码被序列化,所以加上transient关键字
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "JavaBeansForUser{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
2.序列化该对象
package com.ztc.serializable;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Test {
private static final String path = "D:\\csdnStudy\\testSerializable.txt";
//序列化对象
protected static void Serialization() {
JavaBeansForUser user = new JavaBeansForUser();
user.setUsername("昨夜冻成狗");
user.setPassword("123456789");
System.out.println(user);
//序列化对象到文件中
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream(path));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Serialization();
}
}
此时打开输出的文件。
文件的内容显示为乱码,无需管它。
2.反序列化该对象
protected static void Deserialization() {
File file = new File(path);
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
JavaBeansForUser newUser = (JavaBeansForUser) ois.readObject();
System.out.println(newUser.toString());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Deserialization();
}
此时,控制台输出:
JavaBeansForUser{username='昨夜冻成狗', password='null'}
可见,被transient修饰的password属性是null。
3.修改JavaBeansForUser对象(增加state属性),并重新生成serialVersionUID
private static final long serialVersionUID = -3013852264298187198L;//新生成的serialVersionUID
// private static final long serialVersionUID = 6351505220758788992L;
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
4.再次反序列化原文件
控制台打印错误:
java.io.InvalidClassException: com.ztc.serializable.JavaBeansForUser; local class incompatible: stream classdesc serialVersionUID = 6351505220758788992, local class serialVersionUID = -3013852264298187198
可见,当修改实体类,并修改serialVersionUID后,原文件不会被反序列化,需要重新序列化,才可以反序列化。
注:本文多有参考《java编程思想》。