概念
序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。
这两个过程结合起来,可以轻松地存储和传输数据。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
目的
1、以某种存储形式使自定义对象持久化,在MVC模式中很是有用;
2、将对象从一个地方传递到另一个地方;
实现方法
实现 java.io.Serializable 接口
序列化时,需要用到对象输出流ObjectOutputStream ,然后通过文件输出流构造 ObjectOutputStream 对象调用writeObject写入到文件
反之,反序列化时用到对象输入流ObjectIntputStream, 然后通过文件输出流构造 ObjectIntputStream对象调用readObject加载到内存
注意
如果你先序列化对象A后序列化B,那么在反序列化的时候一定记着JAVA规定先读到的对象是先被序列化的对象,不要先接收对象B,那样会报错。
实现序列化的要求
只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常。
实现Java对象序列化与反序列化的方法
假定一个Student类,它的对象需要序列化,可以有如下三种方法:
方法一:若Student类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化
ObjectOutputStream采用默认的序列化方式,对Student对象的非 transient的实例变量进行序列化。
ObjcetInputStream采用默认的反序列化方式,对Student对象的非transient的实例变量进行反序列化。
方法二:若Student类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。
ObjectOutputStream调用Student对象的writeObject(ObjectOutputStream out)的方法进行序列化。
ObjectInputStream会调用Student对象的readObject(ObjectInputStream in)的方法进行反序列化。
方法三:若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。
ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。
ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。
序列化
//创建一个对象输出流,将对象输出到文件
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream(fileName));
out.writeObject("序列化日期是:"); //序列化一个字符串到文件
out.writeObject(new Date()); //序列化一个当前日期对象到文件
UserInfo user=new UserInfo("renyanwei","888888",20);
out.writeObject(user); //序列化一个会员对象
out.close();
反序列化
//创建一个对象输入流,从文件读取对象
ObjectInputStream in=new ObjectInputStream(new FileInputStream(fileName));
//注意读对象时必须按照序列化对象顺序读,否则会出错
//读取字符串,对应上边第一个塞进去的东西,一个字符串,也可看做是一个对象
String today=(String)(in.readObject());
System.out.println(today);
//读取日期对象,对应上边第二个塞进去的东西,一个时间的对象
Date date=(Date)(in.readObject());
System.out.println(date.toString());
//读取UserInfo对象并调用它的toString()方法,对应上边第三个塞进去的东西,一个UserInfo的对象
UserInfo user=(UserInfo)(in.readObject());
System.out.println(user.toString());
in.close();
关于序列化,还有几个问题需要注意
1、序列化 ID 问题
比如,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。
原因就是,虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化ID 是否一致(就是 private static final long serialVersionUID = 1L)。即使两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。
看代码理解private static final long serialVersionUID
importjava.io.Serializable;
publicclassAimplementsSerializable {
privatestaticfinallongserialVersionUID = 1L;
privateString name;
publicString getName() {
returnname;
}
publicvoidsetName(String name) {
this.name = name;
}
}
importjava.io.Serializable;
publicclassAimplementsSerializable {
privatestaticfinallongserialVersionUID = 2L;
privateString name;
publicString getName() {
returnname;
}
publicvoidsetName(String name) {
this.name = name;
}
}
特性使用案例
想必很多人了解设计模式中的门面模式吧,也叫作Façade模式
门面模式
Façade模式是为一个复杂的模块或者子系统提供一个供外界访问的接口
相当于外界对于子系统的操作就是黑箱子操作
比如...
Client 端通过Façade Object 才可以与业务逻辑对象进行交互。而客户端的 Façade Object 不能直接由 Client 生成,而是需要Server 端生成,然后序列化后通过网络将二进制对象数据传给 Client,Client 负责反序列化得到 Façade 对象。
该模式可以使得Client 端程序的使用需要服务器端的许可,同时 Client 端和服务器端的 Façade Object类需要保持一致。当服务器端想要进行版本更新时,只要将服务器端的 Façade Object 类的序列化 ID 再次生成,当 Client端反序列化 Façade Object 就会失败,也就是强制 Client 端从服务器端获取最新程序。
Transient 关键字问题
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值还是原来的值
Externalizable
无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。
JDK中提供了另一个序列化接口--Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效,此时使用该接口时,序列化的细节需要由程序员去完成。
public class Person implements Externalizable {
private String name =null;
transientprivateInteger age =null;
privateGender gender =null;
publicPerson() {
System.out.println("none-arg constructor");
}
publicPerson(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
privatevoidwriteObject(ObjectOutputStream out)throwsIOException {
out.defaultWriteObject();
out.writeInt(age);
}
privatevoidreadObject(ObjectInputStream in)throwsIOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
@Override
publicvoidwriteExternal(ObjectOutput out)throwsIOException {
}
@Override
publicvoidreadExternal(ObjectInput in)throwsIOException, ClassNotFoundException {
}
}
如上所示的代码,由于writeExternal()与readExternal()方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。
另外,使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中Person类的无参构造器会被调用。
由于这个原因,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。
对上述Person类进行进一步的修改,使其能够对name与age字段进行序列化,但忽略掉gender字段,如下代码所示:
public class Person implements Externalizable {
private String name = null;
transient private Integer age = null;
private Gender gender = null;
public Person() {
System.out.println("none-arg constructor");
}
public Person(String name, Integer age, Gender gender) {
System.out.println("arg constructor");
this.name = name;
this.age = age;
this.gender = gender;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
...
}
对敏感字段加密
问题背景
服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。
解决
在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。
以下是自定义在 writeObject和readObject加密解密的过程
private void writeObject(ObjectOutputStream out) {
try {
PutField putFields = out.putFields();
System.out.println("原密码:" + password);
password = "encryption";//模拟加密
putFields.put("password", password);
System.out.println("加密后的密码" + password);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
GetField readFields = in.readFields();
Object object = readFields.get("password", "");
System.out.println("要解密的字符串:" + object.toString());
password = "pass";//模拟解密,需要获得本地的密钥
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
参考文献:深入理解java序列化