序列化Serializable和Parcelable
概念
Java中的序列化是一种将对象持久化(比如存储在磁盘)的手段。一般情况下,程序运行(即JVM运行)时,Java对象(短暂)存储在内存中。但JVM停止运行后,对象的状态信息就不能保存在内存了。我们需要将对象持久化保存,这就是序列化的用途。
简单说,序列化就是,将对象的状态信息转换为字节序列的过程。通过序列化,可以将对象的状态信息转换为字节序列,然后通过IO流保存到磁盘或者网络传输。然后从磁盘或网络请求获取到字节流,通过反序列化,重新创建该对象。
Serializable
Serializable是Java提供的序列化接口,实现Serializable接口的类,极为可序列化的类。可以直接通过ObjectOutputStream序列化,也可以通过ObjectInputStream反序列化。
以下便是Java序列化的典型用法
public class SerializableTest implements Serializable {
private final static long serialVersionUID = 1L;
private String name;
private String job;
private UnSerializable unSerializable;
public SerializableTest(String name, String job,UnSerializable unSerializable) {
this.name = name;
this.job = job;
this.unSerializable = unSerializable;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public UnSerializable getUnSerializable() {
return unSerializable;
}
public void setUnSerializable(UnSerializable unSerializable) {
this.unSerializable = unSerializable;
}
}
serialVersionUID
serialVersionUID是类的序列化ID,可以理解为这个类的版本号。serialVersionUID可以自己手动声明,也可以由系统默认。系统默认的serialVersionUID是根据类的定义计算出一个serialVersionUID。如果类的定义发生了改变,默认的serialVersionUID也会随之变化,就像类的版本发生了改变一样。
serialVersionUID的作用在于,虚拟机是否允许反序列化,不仅仅是取决于类的路径和功能代码是否一致。还有一个非常重要的判断依据是,两个类的serialVersionUID是否一致。
具体的场景就是:客户端A和B通过网络传递对象数据,客户端A将c对象序列化为字节流,通过网络传输给客户端B,然后B反序列化得到c。此时如果A和B端的app版本不同,而导致C类的serialVersionUID不同,就会导致B端反序列化失败。
当然,有时候,我们也可以通过这种方法,强制app的低版本升级
Java官方是建议我们手动声明serialVersionUID。一般情况下没有特殊需求,serialVersionUID就直接声明为1L。
成员变量序列化
一个对象的成员变量是不可序列化的对象的引用,会导致对象序列化失败,抛出NotSerializableException异常。序列化要求,对象的成员变量也是可序列化的。
静态变量序列化
序列化保存的是对象的状态信息,静态变量属于类的状态,因此,序列化不保存静态变量。
父类的序列化
一个子类实现了Serializable接口,它的父类没有实现Serializable接口。序列化该子类对象,然后反序列化输出其父类定义的变量的值,该变量的值和序列化之前不同。
如果父类没有实现Serializable接口,虚拟机是不会序列化父类对象的。此时就要求,父类必须有无参构造函数。而一个Java对象的构造必须先有父类对象,再创建子类对象,反序列化时也不例外。所以反序列化时,虚拟机只能调用父类的无参构造函数来创建父类对象。这样,父类定义的成员变量的值,就是调用无参构造函数后的值。如果你考虑到了这种序列化的情况,就可以在父类的无参构造函数对成员变量赋值。
Transient
Transient关键字的作用就是控制变量的序列化,在变量声明前声明Transient,表示该变量不会被序列化。在反序列化时,该变量值就会是该类型的初始值,如int型为0,String型为null。
自定义readObject和WriteObject
将对象序列化后,进行网络传输,就要考虑到数据的安全性问题。这样的场景,可以考虑封装自定义的writeObject和readObject方法。这样可以序列化时,对数据进行加密,反序列化时解密。
public class SerializableTest implements Serializable {
private final static long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private void writeObject(ObjectOutputStream out) {
try {
ObjectOutputStream.PutField putFields = out.putFields();
System.out.println("原name:" + name);
name = "encryption-"+name;//模拟加密
putFields.put("name", name);
System.out.println("加密后的name" + name);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
ObjectInputStream.GetField readFields = in.readFields();
Object object = readFields.get("name", "");
System.out.println("要解密的字符串:" + object.toString());
name = object.toString().split("-")[1];//模拟解密
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
序列化存储规则
Java序列化机制为了节省磁盘空间,具有特定的存储规则,当多次序列化写入文件的是同一对象时,并不会再将对象的内容写入文件,而只是再存储一个引用,此时写入文件的就是新增的引用和一些控制信息。反序列化时,恢复引用关系,使得所有的引用指向的是同一个对象。这样的特点,会带来另一个值得注意的问题:
File file = new File("H:/sourceCode/workspace4java/test.txt");
SerializableTest testObject = new SerializableTest("Smith","engineer",32,new UnSerializable("1024"));
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(testObject);
oos.flush();
testObject.setName("James");
oos.writeObject(testObject);
oos.close();
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
SerializableTest result1 = (SerializableTest)ois.readObject();
SerializableTest result2 = (SerializableTest)ois.readObject();
System.out.println(result1.getName());
System.out.println(result2.getName());
输出结果是
Smith
Smith
我们希望看到的是第二次反序列化的对象name为James,但是可以看到,两次反序列化的对象的name一样。这是因为第二次序列化写入时,虚拟机根据引用关系,已经判断出写入的是同一对象,因此只保存了第二次的引用。所以反序列化读取时,都是第一次保存的对象。
Parcelable
Parcelable是Android提供的序列化接口。它相比于Serializable的优点是,序列化的效率要高一些。 实现了 Parcelable 接口的类在序列化和反序列化时会被转换为 Parcel 类型的数据 。 Parcel 是一个载体,它可以包含数据或者对象引用,然后通过 IBinder 在进程间传递。
Parcelable和Serializable的对比:
- 一般在保存数据到 SD 卡或者网络传输时建议使用 Serializable 即可,虽然效率差一些,好在使用方便。
- 而在运行时数据传递时建议使用 Parcelable,比如 Intent,Bundle 等,Android 底层做了优化处理,效率很高。
本文参考:
[https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html
https://www.hollischuang.com/archives/1140
https://blog.csdn.net/u011240877/article/details/72455715