1.序列化
序列化是指将对象按照某种协议格式转化为二进制字节序列;对应的,反序列化将二进制字节序列恢复为对象。
序列化常用于两种情况:
- 数据持久化。利用序列化将对象持久化到外部存储中,然后在合适的时机,通过反序列化恢复为对象。目前此种用法较少,使用数据库持久化是更好的选择。
- 网络传输对象。将对象序列化为二进制字节数据,通过网络传输到远程机器,然后通过反序列化恢复为对象。这是序列化使用的主要场景,远程方法调用RMI和远程过程调用RPC都依赖于序列化。
JAVA序列化的设计初衷是为了保存对象的状态从而保存进程状态,以便在合适的时机恢复进程,但目前已基本脱离这种用法。JAVA序列化的另一大用法是远程过程调用RMI,由于性能不高(序列化后所占存储空间过大且序列化耗时较高)的原因也很少用于实际生产环境中。生产环境中,建议使用性能更高的序列化方案,比如Google的ProtoBuf
、FaceBook的Thrift
,以及用于HTTP的JSON
,XML
等等。尽管原生JAVA序列化方案已较少使用,但理解其原理也大有裨益。
2. 序列化的基本用法
在JAVA中,一个对象要实现序列化是极其简单的:实现Serializable
接口即可。
class User implements Serializable {
private String name;
private String password;
public User(String name, String password) {
this.name = name;
this.password = password;
System.out.println("constructor");
}
}
然后再配合ObjectOutputStream
和ObjectInputStream
即可实现对象的序列化和反序列化,一个简单的使用示例如下:
public static void main(String[] args) throws Exception {
User user = new User("root", "root");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(outputStream);
out.writeObject(user);
out.close();
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(outputStream.toByteArray()));
User user1 = (User) in.readObject();
System.out.println(user1);
}
在示例中,实现了将User
对象序列化到ByteArrayOutputStream
中的字节数组中,然后使用反序列化将字节数组恢复为一个新的对象。一般的,也可以将对象序列化到FileOutputStream
中实现持久化。
再次回顾示例代码,需要注意以下几点:
2.1 Serializable
是一个标记接口
Serializable
只是一个标记接口,其中没有任何方法。但如果一个待序列化对象不实现该接口,将会抛出NotSerializableException
异常。其原理,可参考JDK的ObjectOutputStream源码:
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
throw new NotSerializableException(cl.getName());
}
由此,可知除了实现Serializable
接口的对象外,String
、Array
和Enum
也都可以序列化。另外,代码中没有列出的JAVA基本数据类型:int
、char
、double
等也默认可序列化。
2.2 可使用serialVersionUID
进行版本控制
如果跨进程使用序列化机制,一个基本要求是:两个进程含有相同的对象class文件,相同指对象的功能和类路径都必须一致。除此之外,两个类的序列化ID也必须一致。JAVA序列化机制使用一个如下的变量serialVersionUID
表示序列化ID:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
如果两个进程中该值不一致,反序列化时会抛出InvalidClassException
异常,从而提醒用户该对象的类可能已被修改了某些字段,需要更新class文件。
注意最开始的示例中没有指定这个变量,此时,编译器会根据类名、接口以及其中所含字段自动生成一个默认值。这个默认值随编译器的实现细节不同而不同,所以强烈建议显式指明该变量值。其中访问权限修饰符建议使用private
,一个可序列化对象完整的示例如下:
class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String password;
}
显式声明该变量值的另一个好处是:可以利用序列化的兼容机制,即:
- 新增一个字段。
- 改变字段的访问权限修饰符。
public
、protected
、package
和private
不会影响序列化。 - 将一个字段从
static
修改为非static或者transient
修改为非transient。
这几种情况下,反序列化的class不必和序列化时的class代码完全一致。但如果使用默认serialVersionUID
则会生成不一致的值,从而抛出InvalidClassException
异常。
2.3 子类的序列化
当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。当一个子类实现序列化,而父类没有实现序列化,情况又会怎么样呢?
class User {
private String name;
private String password;
// 构造方法省略
}
class Vip extends User implements Serializable{
private int money;
public Vip(String name, String password, int money) {
super(name, password);
this.money = money;
}
}
序列化正常,但反序列化时抛出如下异常:
Exception in thread "main" java.io.InvalidClassException: serial.Vip; no valid constructor
为了实现反序列化,有两种方法:
- 父类也实现
Serializable
接口,此时完美符合序列化机制要求。 - 父类不实现
Serializable
接口,但提供一个无参构造方法。此时,子类中的字段可以恢复为序列化前的值,但父类中的字段为初始化默认值。
方法2的示例输出如下:
Vip{money=5,name=null,password=null}
2.4 静态变量的序列化
JAVA序列化保存的是对象的状态,而静态变量不是对象的状态而是类的状态,所以序列化不保存静态变量。
2.5 反序列化不会调用构造方法
JAVA在反序列化时不会调用构造方法。当类有继承关系时,情况又稍有不同。正如2.3的示例,如果子类实现Serializable
接口而父类并未实现,但父类有无参构造方法时,尽管子类的构造方法不会被调用,但父类的无参构造方法将会被调用。给User
和VIP
提供如下的构造方法:
public User() {
System.out.println("user no-arg constructor");
}
public Vip(String name, String password, int money) {
super(name, password);
this.money = money;
System.out.println("vip constructor");
}
则2.3示例反序列化的输出为:
user no-arg constructor
Vip{money=5,name=null,password=null}
可见,子类的构造方法不会被调用,但父类的无参构造方法被调用。
3.自定义序列化
3.1 transient
特殊情况下,某些字段并不希望被序列化,比如说User
类中的密码password
字段,并不希望被序列化到文件中。也许你会联想到前述子类序列化的示例,父类的字段不会被序列化,可以实现该需求。但这不是好的方式,JAVA提供了一种更好的方法:使用transient
关键字。示例如下:
class User implements Serializable{
private static final long serialVersionUID = 42L;
private String name;
private transient String password;
// 构造方法和getter setter省略
}
在不希望被序列化的字段前添加transient
关键字,序列化时将忽略该字段。示例的输出如下:
User{name='root', password='null'}
3.2 writeObject和readObject
在上述的密码password
字段的处理中,加密该字段然后序列化到文件是一种更好的方法。可通过在待序列化类添加如下的方法实现:
class User implements Serializable {
private static final long serialVersionUID = 42L;
private String name;
private transient String password;
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeUTF(encrypt(password));
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
String encryptStr = in.readUTF();
System.out.println("encrypt=" + encryptStr);
this.password = decrypt(encryptStr);
}
}
注意其中的writeObject()
和readObject()
方法,可用这两个方法对序列化和反序列化进行特殊处理。本例中,序列化时对密码加密,反序列化时进行解密。本例的输出如下:
encrypt=63A9F0EA7BB98050796B649E85481845
User{name='root', password='root'}
如果要实现自己的特殊处理,注意这两个方法的方法签名必须和示例一致,其中访问权限修饰符注意为private
,JAVA是通过反射调用的这两个方法。此外,defaultWriteObject()
和defaultReadObject()
是调用JAVA序列化的默认实现。
3.3 Externalizable
第三种自定义序列化过程的方法是:实现Externalizable
接口,该接口是Serializable
的子接口,代码如下:
public interface Externalizable extends java.io.Serializable {
// 序列化
void writeExternal(ObjectOutput out) throws IOException;
// 反序列化
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
如果序列化时实现该接口,则需要自主控制所有序列化细节。也就是说,该接口是一个正常的接口,使用时遵循JAVA语法即可。作为标记接口的Serializable
反序列化时不会调用构造方法,而实现该接口反序列时则需要调用类的无参构造方法,然后调用readExternal()
方法。使用该接口序列化的示例如下:
class User implements Externalizable {
private static final long serialVersionUID = 42L;
private String name;
private transient String password;
public User() {}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeUTF(encrypt(password));
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = in.readUTF();
String encryptStr = in.readUTF();
System.out.println(encryptStr);
this.password = decrypt(encryptStr);
}
}
最后附上JAVA序列化相关的资料: