前言
对象的序列化也是有着特殊的应用场景的,我想大概有如下场景需要对象的序列化:
- 对象本地持久化
- 对象通过Socket 进行网络传输
- 深拷贝
序列化方式:
- Serializable:java提供
- Parcelable:安卓提供
一 、Serializable
1、简介:
Serializable是java提供的一个序列化的接口,这个接口是个空实现。为对象提供标准的序列化和反序列化操作。
2、栗子
定义一个类实现Serializable接口,然后声明个serialVersionUID即可。甚至serialVersionUID也不是必须的。我们不声明这个值也可以完成序列化,但是会对反序列化产生影响。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
public String userName;
public int userId;
public boolean isMale;
public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
}
}
private void serializableTest() throws Exception {
User user = new User(0, "Tom", true);
//对象写入文件 ---序列化过程
ObjectOutputStream oos = new ObjectOutputStream(openFileOutput("cache.txt",MODE_PRIVATE));
oos.writeObject(user);
oos.close();
// 对象读取 --- 反序列化过程
ObjectInputStream ois = new ObjectInputStream(openFileInput("cache.txt"));
User newUser = (User) ois.readObject();
ois.close();
Log.i(TAG, "newUser:"+newUser.userName);//newUser:Tom
Log.i(TAG, "newUser:"+newUser.userId);//newUser:0
Log.i(TAG, "newUser:"+newUser.isMale);//newUser:true
}
3、serialVersionUID
1、serialVersionUID的工作机制:
序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中,当反序列化的时候系统会检测这个文件中的serialVersionUID是否和当前类的serialVersionUID一致,如果一致这个时候可以反序列化成功,否则就说明当前类和序列化的类相比发生了某些变化。比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的,序列化异常(InvalidClassException ).
2、serialVersionUID手动指定区别1、手动指定serialVersionUID时:
这样序列化和反序列化时的serialVersionUID是相同的,因此可以正常反序列化。即使当类结构发生变化时,系统也会尽可能的恢复原有类结构,也不至于InvalidClassException 异常
2、未指定serialVersionUID的值时:
反序列化时当前类有所改变,比如增加或减少了某些成员变量,那么系统会重新计算当前类的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash。
所以当我们手动指定了serialVersionUID的值,就可以很大程度上避免了反序列化的失败。但是如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过了,但是反序列化还是会失败,因为类结构发生了毁灭性的改变。
4、serialVersionUID生成方式:
- 一是默认的1L,比如:private static final long serialVersionUID = 1L;
- 二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。这点可利用编译器提示自动生成如下图。
二者区别:
1L这种主要是用于区分系统版本号的,随着版本号的增加,可以改成2L、3L等。但程序很有可能升级版本,如果使用了1L、2L导致序列号不同,有可能第二版会不兼容第一版的程序,但是64位的哈希字段方式就可以,只要这个类本身没有修改过,两个相同的序列号都会指向同一个类。
5、注意
- 静态成员变量不属于对象,所以不会参与序列化过程。其次用transient关键字标记的成员变量不参与序列化过程。
- 一个子类实现了 Serializable 接口,而父类没有实现 Serializable 接口,则父类不具有序列化功能。
- 父类实现序列化,子类则也可以具有序列化功能。
- 当对一个对象进行序列化两次写入文件。第二次写入文件只增加了 5 个字节,并且两个对象是相等的。Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件是同一个对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,所以两者引用是相等的。
6、原理简介
1、Serializable 的原理是通过 ObjectInputStream 和 ObjectOutputStream 来实现的,整个序列化过程使用了大量的反射和临时变量,而且在序列化对象的时候,不仅会序列化对象本身,还需要递归序列化对象引用的其他对象。
7、序列化相关方法
- writeObject &readObject
- writeReplace &readResovle
1、在序列化过程中,如果实现序列化接口的类中定义了 writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。如果没有这样的方法,则默认调用的是 ObjectOutputStream 的 defaultWriteObject 方法和 ObjectInuputStream 的 defaultReadObject 方法。
2、如果实现序列化接口类中了定义writeReplace 和 readResovle 方法。这两个方法代理序列化的对象,可以实现自定义返回的序列化实例。通过它们我们可以实现对象序列化的版本兼容,在反序列化生成新的对象的时候会破坏单例,我们可以添加一个 readResolver 方法直接返回单例对象即可。例如【java设计模式】单例设计模式(Singleton)中第三小节。
ps:自定义序列化可对字段做特殊处理,例如实现序列化加密。
二、Parcelable接口
1、栗子
/**
* Create by SunnyDay on 2019/03/31
*/
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public Book book;
public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
protected User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(userId);
dest.writeString(userName);
dest.writeByte((byte) (isMale ? 1 : 0));
dest.writeParcelable((Parcelable) book, 0);
}
}
定义类实现Parcelable接口即可,根据编译器提示实现Parcelable接口中的方法即可,再根据提示实现CREATOR完成固定逻辑。
2、重载方法
方法 | 功能 |
---|---|
createFromParcel(Parcel in) | 从序列化后的对象中创建原始对象 |
writeToParcel(Parcel dest, int flags) | 将当前对象写入序列化结构中,其中flags的标识有0或者1,为1标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0。 |
newArray(int size) | 创建指定长度原始类型数组。 |
User(Parcel in) | 从序列化后的对象中创建原始对象。此构造编译器提示帮你生成。 |
describeContents | 返回对象的内容描述,如果含有文件描述符返回1否则返回0,一般为0 |
3、序列化对象最为成员变量
在 User(Parcel in)中book是另一个可序列化对象(作为本类的成员),所以他的反序列化过程需要传递当前线程上下文的类加载器。否则报无法找到类的错误。
三、 Parcelable和Serializable区别:
序列化方式 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
Serializable | 使用简单、不需要手动去处理序列化和反序列化的过程 | 开销非常大,序列化和反序列化过程需要大量io操作 | 将对象序列化到设备中或者将对象序列化后通过网络传输 |
Parcelable | 效率高 | 使用起来麻烦、需要手动去处理序列化和反序列化的过程 | 内存上进行序列化 |