在开发中经常会用 Intent 和 Binder 进行数据传输时就要用到 Parcelable 和 Serializable,以及有的时候需要把对象持久化到存储设备上,就需要用到 Serializable。
Serializable 的使用比较简单,我们先来介绍如何使用 Serializable 来完成对象的序列化
Serializable
Serializable
是 Java 提供的一个序列化接口, 是一个空接口。需要序列化的对象只需要实现这个接口,并且选择性地声明 serialVersionUID
即可,当然也可以不声明。
public class User implements Serializable {
private static final long serialVersionUID = 9187239014106741L;
private int age;
private String name;
private boolean isMale;
...
}
序列化和反序列化
User 类实现了 Serializable
接口, 因此它是可以被序列化和反序列化的, 从上面的实现来看 是非常简单的。然后进行对象的序列化和反序列化也非常简单,通过 ObjectOutputStream
和 ObjectInputStream
可轻松实现:
// 序列化
User user = new User();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.text"));
User newUser = (User) in.readObject();
in.close();
只需要实现 Serializable
的 User 对象写到文件然后读取文件得到反序列化的新对象 newUser。得到的新对象 newUser
和原来的 user
内容完全一样,但两者不是同一个对象。
而对于系统的默认序列化和反序列化过程,则是内部执行 writeObject
和 readObject
来实现的。我们可以通过重写这两个方法来改变系统的默认序列化过程,一般不用自己重写:
private void readObject(java.io.ObjectOutputStream) throws IOException{
// ...自定义反序列化代码
}
private void writeObject(java.io.ObjectInputStream) throws IOException, ClassNotFoundException{
// ...自定义序列化代码
}
serialVersionUID 工作机制
上面提到 serialVersionUID
可以选择性实现,那这个到底是干嘛的,在序列化和反序列化过程中起到了什么作用呢?
理论上来讲,既然系统提供了的并且没有废弃,那它必须是有用的。serialVersionUID 是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的 serialVersionUID 和当前类的 serialVersionUID 相同才能被正常的反序列化。
* serialVersionUID 具体的工作机制是这样的:序列化的时候,系统会把当前类的 serialVersionUID
写入序列化的文件中(也可能是其它中介),当反序列化的时候系统会检查文件中的 serialVersionUID 是否和当前类的 serialVersionUID 一致,如果一致则反序列化成功,否则会报错提示 serialVersionUID 不一致。
尽量手动指定
serialVersionUID
的值,类型是long
,用public static
修饰。如果不手动指定,系统会根据类的 hash 值去计算出一个serialVersionUID
值。这是如果反序列化时当前类有所改变,比如增加或删除某个成员变量,系统就会重新计算当前类的 hash 值,这时当前类的serialVersionUID
和序列化数据中的serialVersionUID
就不一致,则反序列化失败,导致系统 crash。
所以我们手动指定serialVersionUID
的值,可以很大程度上避免反序列化过程的失败。必须版本升级后我们删除了某个成员变量或增加了一些成员变量,这时反序列化依然会成功并最大限度的恢复了数据。但是如果类的结构发生了非常规性改变,比如修改类名、某个字段的类型改变,这时即使serialVersionUID
验证通过,反序列化依然失败。
static 和 transient 修饰的变量
序列化只对类中的 filed 字段属性进行序列化,且序列化的只是类的实例对象的属性类型及值。
* static:静态变量属于类,被所有对象共享,非某个对象私有,不会被持久化
* transient:代表瞬间的意思,表示不会被持久化
Parcelable
Parcelable
是安卓提供的一个序列化接口, 相比 Serializable
, Parcelable
的使用相对比较复杂, 但是效率要比 Serializable
高很多。
下面是 Parcelable 典型用法:
public class User implements Parcelable {
private int age;
private String name;
private boolean isMale;
private Book book;
private User(Parcel in){
age = in.readInt();
name = in.readString();
isMale = in.readInt() == 1;
in.readParcelable(Thread.currentThread().getContextClassLoader())
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(age);
out.writeString(name);
out.writeInt(isMale ? 1 : 0);
out.writeParcelable(book, 0);
}
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>(){
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[0];
}
};
}
可以看到 Parcel
类在序列化和反序列化过程中有中有重要的作用,它内部封装了可序列化的数据,Parcel
可以在 Binder 中自由传输。整个序列化过程是通过 writeToParcel()
方法来实现的, 内部用 Parcel
的一系列write方法来完成;反序列化功能则由 CREATOR
实现的,内部标明了如果创建序列化对象和数组,并通过 Parcel
的一系列read完成反序列化。describeContents()
是内容描述,仅当当前对象存在文本描述符时返回1,其它情况都返回0。
- 需要注意的是,在
User(Parcel in)
方法中,因为 book 是另一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器,否则会报找不到类异常。
系统已经为我们提供了很多实现 Parcelable
接口的类,Intent、Bundle、Bitmap
等,以及 List
和 Map
也可以序列化,前提是它的每个元素都是可序列化的。
Parcelable 和 Serializable 区别
Serializable
是 Java 的序列化接口,使用简单,但是开销大,序列化和反序列都需要用到大量的 IO 操作,因此效率较低,通过序列化到存储设备中,可以实现持久化。Parcelable
是 Android 提供的序列化接口,也是 Android 推荐的序列化方式,因此是首选。使用比较麻烦,效率高,主要序列化到内存。
虽然
Parcelable
的性能要强于Serializable
,但是仍然有特殊的情况需要使用 Serializable ,而不去使 用 Parcelable。因为 Parcelable 无法将数据进行持久化,因此在将数据保存在磁盘的时候,仍然需要使用后者,因为前者无法很好的将数据进行持久化。(原因是在不同的Android版本当中,Parcelable 可能会不同,因此数据的持久化方面仍然是使用 Serializable