序列化和持久化很相似,有些人甚至混为一谈,其实还是有区别的,序列化是为了解决对象的传输问题,传输可以在线程之间、进程之间、内存外存之间、主机之间进行。在安卓中绝大部分场景是通过Binder来进行对象的传输.
serialVersionUID:
- 如果没有指定UID,那么JDK会自动帮我们生成一个UID.手动指定这个UID可以减少运行时的开销,参照EffectiveJava中的描述
这消除了序列版本 UID 成为不兼容性的潜在来源(详见第 86 条)。这么做还能获得一个小的性能优势。如果没有提供序列版本 UID,则需要执行高开销的计算在运行时生成一个 UID。
- 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致
读者应该听过 Façade 模式,它是为应用程序提供统一的访问接口,案例程序中的 Client 客户端使用了该模式,案例程序结构图如图 1 所示。
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 端从服务器端获取最新程序。
serialVersionUID是用来验证版本一致性的。做兼容性升级的时候,不要改变serialVersionUID. 做强制升级的时候,需要改变serialVersionUID让客户端(使用者)来重新序列化兼容.
保护字段不被序列化:
- transient关键字修饰的变量,不会被序列化
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。 - static关键字修饰的也不会被序列化, 因为static是类的特征,和对象无关
- 放在父类中
如果父类实现了序列化,子类默认实现序列化.
要想将父类对象也序列化,就需要让父类也实现Serializable 接口。如果父类不实现的话的,就 需要有默认的无参的构造函数。在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。
自定义序列化
通过手动实现readObject
,writeObject
,readResolve
方法,可以干预Serializable的序列化流程.
源码中的实现
我们在实现这些方法的时候,都是private的,这样做一是为了对外隐蔽细节,二是你不用private是不会被调用的,这个最后有解释.
看看源码中是怎么处理的,以ArrayList
为例
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
//ArrayList就是数组实现的,注意这里使用了transient关键字
transient Object[] elementData; // non-private to simplify nested class access
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
}
elementData
是transient
修饰的,但是我们和平时一样从反序列化的结果中使用get(index)的时候,还是能够获取elementData
,测试一下:
public static void testArrayList(){
String path = "D:/Test2.txt";
ArrayList<String> lists = new ArrayList<>();
lists.add("1");
lists.add("2");
lists.add("3");
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(path));
outputStream.writeObject(lists);
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(path));
ArrayList<String> datas = (ArrayList<String>) inputStream.readObject();
System.out.println(datas.get(1));
System.out.println("lists==datas : " + (lists == datas));
} catch (Exception e) {
e.printStackTrace();
}
}
输出为
2
lists==datas : false
这里我们看到,ArrayList通过自定义writeObject
和readObject()
方法,实现了transient修饰的数组的序列化和反序列化. 为什么ArrayList要这么干呢?
这是因为Java的容器类在设计的时候,都是动态扩容的,例如ArrayList,实际数组内包含着大量未使用的空间, 为了不造成序列化和反序列化时的资源浪费. Java采用了这样的策略: 把elementData声明为transient避免序列化时占用大量无效的空间,同时通过自定义的序列化和反序列化策略:
writeObject()
时,先申请size大小的流,把数组中真正的元素写入(排除空的)
readObject()
时,从流中读取对象并保存到elementData数组中
这么做,既保证了序列化的正常使用,又节省了磁盘和流的开销.
readObject和writeObject
引用EffectiveJava里的一段话
不严格的说, readObject 方法是一个“用字节流作为唯一参数”的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, readObject 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这是利用普通构造器无法创建的。
对应读写,这里以readObject()
为例看看我们自己定义的方法是怎么被调用的
- 调用ObjectInputStream的
public final Object readObject()
方法从流中读取对象,其中调用了Object obj = readObject0(false);
- 调用
private Object readObject0(boolean unshared) throws IOException
private Object readObject0(boolean unshared) throws IOException {
....
switch (tc) {
case TC_OBJECT: //type取决于writeObject的类型,这里是Object类型
return checkResolve(readOrdinaryObject(unshared));
}
....
}
private Object readOrdinaryObject(boolean unshared)
private Object readOrdinaryObject(boolean unshared){
...
//1.调用ObjectStreamClass的无参构造
ObjectStreamClass desc = readClassDesc(false);
ClassNotFoundException resolveEx = desc.getResolveException();
if (desc.isExternalizable()) {
//如果ObjectStreamClass实现了Externalizable接口
readExternalData((Externalizable) obj, desc);
} else {
//默认没有实现Externalizable接口走这里
readSerialData(obj, desc);
}
...
}
-
readClassDesc(false)
调用了ObjectStreamClass的无参构造
private ObjectStreamClass readClassDesc(boolean unshared){
...
case TC_CLASSDESC:
return readNonProxyDesc(unshared);
}
private ObjectStreamClass readNonProxyDesc(boolean unshared){
//这里调用了ObjectStreamClass的无参构造
ObjectStreamClass desc = new ObjectStreamClass();
}
-
private void readSerialData(Object obj, ObjectStreamClass desc)
这里才是读取序列化的逻辑,参考注释 从流里面读取object,从父类到子类(这也印证了父类实现序列化,子类也会被序列化)
Reads instance data for each serializable class of object in stream, from superclass to subclass.
private void readSerialData(Object obj, ObjectStreamClass desc){
...
//这一步实际是再次调用了ObjectStreamClass的有参构造方法
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
//判断是否手动实现了readObject()方法
if (slotDesc.hasReadObjectMethod()){
slotDesc.invokeReadObject(obj, this); //invoke,反射调用
}
...
}
public class ObjectStreamClass implements Serializable {
ClassDataSlot[] getClassDataLayout() throws InvalidClassException {
// REMIND: synchronize instead of relying on volatile?
if (dataLayout == null) {
dataLayout = getClassDataLayout0();
}
return dataLayout;
}
private ClassDataSlot[] getClassDataLayout0(){
...
ObjectStreamClass.lookup(c, true), false);
...
}
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
...
try { //这里调用了ObjectStreamClass的有参构造
entry = new ObjectStreamClass(cl);
} catch (Throwable th) {
entry = th;
}
...
}
//在有参构造里,反射获取readObJect的Method对象
/** class-defined readObject method, or null if none */
private Method readObjectMethod;
private ObjectStreamClass(final Class<?> cl) {
if (serializable) {
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
}
}
//这里我们可以看到为什么自定义的方法要声明为private !!!!
private static Method getPrivateMethod(Class<?> cl, String name,
Class<?>[] argTypes,
Class<?> returnType) {
return ((meth.getReturnType() == returnType) &&
((mods & Modifier.STATIC) == 0) &&
((mods & Modifier.PRIVATE) != 0)) ? meth : null;
}
}
-
ObjectStreamClass内判断是否手动实现了
readObject()
方法再通过void invokeReadObject(Object obj, ObjectInputStream in)
反射调用
hasReadObjectMethod
先判断是否实现了readObject方法
/** class-defined readObject method, or null if none */
private Method readObjectMethod;
boolean hasReadObjectMethod() {
requireInitialized(); //
return (readObjectMethod != null);
}
invokeReadObject(Object obj, ObjectInputStream in)
反射调用我们自定义的private修饰的 readObject方法
void invokeReadObject(Object obj, ObjectInputStream in)
throws ClassNotFoundException, IOException,
UnsupportedOperationException
{
requireInitialized();
if (readObjectMethod != null) {
try {
readObjectMethod.invoke(obj, new Object[]{ in });
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ClassNotFoundException) {
throw (ClassNotFoundException) th;
} else if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
readResolve
readResolve()
在readObject()
后被调用,同样需要被声明为private,同样是被反射调用
如上面所说,如果一个类实现了序列化,那么通过反序列化的流,我们就可以构建一个新的对象(隐式的构造器). readObject 产生的对象会违反它所属类的约束条件. 最简单的例子, 反序列化一个单例的类,我们可以得到多个对象,这就违背了单例的限制.
public class Singleton implements Serializable {
private static Singleton SINGLETON;
private Singleton() {
}
public static Singleton getInstance() {
if (SINGLETON == null) {
synchronized (Singleton.class) {
if (SINGLETON == null) {
SINGLETON = new Singleton();
}
}
}
return SINGLETON;
}
}
写入磁盘并反序列化Singleton对象
private static void testReadResolve() {
String path = "D:/Test3.txt";
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(path));
outputStream.writeObject(Singleton.getInstance());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(path));
Singleton singleton = (Singleton) inputStream.readObject();
System.out.println("compareResult: " + (Singleton.getInstance() == singleton));
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
compareResult: false
两个对象,已经绕过了我们通过构造创建对象的约束条件(这里是单例)
这时候如果我们实现了readResolve()
方法
private Object readResolve() throws ObjectStreamException {
System.out.println("readResolve");
return SINGLETON;
}
输出结果:
readResolve
compareResult: true
这是为什么呢,来看这一段代码: 如果类实现了readResolve()
方法,就会调用该方法返回序列化的实例.
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
问题
1. 为什么我们自定义的readObject()
, writeObject()
必须是private呢,查看ObjectStream
的getPrivateMethod
方法中有这一句限制
return ((meth.getReturnType() == returnType) &&
((mods & Modifier.STATIC) == 0) &&
((mods & Modifier.PRIVATE) != 0)) ? meth : null;
2. 跨进程的场景
这里要注意的是static
修饰的变量, 上面提到过static是不参与序列化的.
单进程的场景中, 大家都可以共享static的值.
但是如果跨进程传输的时候, 例如我们平时用Intent跨进程传递. 每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,两个进程中的static
在各自的方法区中. 这时候如果你修改了A进程中的值.B进程的不会受到影响.还是原来的值.
3. Enum的序列化
引用官方文档
枚举常量的序列化与普通的可序列化或可外部化的对象不同。枚举常量的序列化形式仅由其名称组成; 常量的字段值不在表单中。要序列化枚举常量,ObjectOutputStream会写入枚举常量名称方法返回的值。要反序列化枚举常量,ObjectInputStream从流中读取常量名称; 然后通过调用java.lang.Enum.valueOf方法获取反序列化的常量,将常量的枚举类型与接收的常量名称一起作为参数传递。与其他可序列化或可外部化的对象一样,枚举常量可以作为随后出现在序列化流中的反向引用的目标。
总结
-
实现Serializable的对象需要显示声明UID,来进行不同版本的兼容检查
(个人觉得这有点像EffectiveJava中的检查参数的有效性, 在服务端改变了UID后,客户端通过UID来匹配是否兼容->不兼容就强制升级) -
transient
不会被序列化.static
不会被序列化(static是类的特征,并不属于对象) -
容器类List,Map...中都手动实现了
readObject
和writeObject
方法,并且把容器类的元素都设置成了transient
, 这么实现是为了避免容器类扩容后的闲置空间被序列化, 造成磁盘/内存的浪费. -
我们自己也可以通过
readObject
,writeObject
,readResolve
方法来改变默认的序列化流程.
网友评论