序列化与反序列化知识
-
序列化与反序列化
-
序列化:
-
概述:将一个数据结构或者对象转成二进制串的过程
-
应用场景:跨进程通信(通过网络进行传输)
-
序列化方案:
- Serializable:java中的
- Parcelable:Android独有的
- 广义序列化:json,xml,protbuf……甚至是自己定义的协议
-
如何选择合理的序列化方案
- 通用性:是否可以跨平台
- 健壮性(鲁棒性):容错
- 可读性/可调试性:序列化后的东西可读不
- 安全性等;
-
Serializable使用:
-
实现Serializable接口
-
这个接口里面它是空的,只是相当于一个标识
-
空接口如何实现序列化:
- ObjectOutPut(输入输出流)
- ObjectStreamClass:描述一个对象的结构
-
-
实现Externallizable接口:这个类是实现了Serializable接口的
- 提供了两个方法:writeExternal(ObjectOutPut out)与readExternal(ObjectOutPut in)
-
-
-
反序列化:
- 将序列化过程中生成的二进制串转换成数据结构或者对象
-
持久化:硬盘
- 将数据结构或者对象存储起来,用的时候将其反序列化出来
-
-
Serializable具体使用
-
代码思路:序列化
-
实现Serializable接口:
static class User implements Serializable
-
编写工具类,借助了对象输出流,将对象转成了二进制
核心就是ObjectOutputStream.writeObject(obj);
synchronized public static boolean saveObject(Object obj, String path) {//持久化 if (obj == null) { return false; } ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream(path));// 创建序列化流对象 oos.writeObject(obj); oos.close(); return true; } catch (IOException e) { e.printStackTrace(); } finally { if (oos != null) { try { oos.close(); // 释放资源 } catch (IOException e) { e.printStackTrace(); } } } return false; }
- 此时对象就存在本地(持久化,调用时将obj对象存入本地的path路径),可以看,但是可读性不高(这个通过二进制数据流写进去的)
-
-
代码思路:反序列化
-
实现Serializable接口:
static class User implements Serializable
-
编写工具类:借助了对象读入流,将二进制转成对象
核心:ObjectInputStream.readObject();
synchronized public static <T> T readObject(String path) { ObjectInputStream ojs = null; try { ojs = new ObjectInputStream(new FileInputStream(path));// 创建反序列化对象 return (T) ojs.readObject();// 还原对象 } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { if(ojs!=null){ try { ojs.close();// 释放资源 } catch (IOException e) { e.printStackTrace(); } } } return null; }
-
-
细节:
-
java使用Serializable接口实现序列化时,需要对象I/O流进行辅助
-
-
-
使用Externallizable接口实现序列化:
-
代码展示:
private static void ExternalableTest() { User user = new User("zero", 18); System.out.println("1: " + user); //将对象I/O流进行一次包装 ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream oos = null; byte[] userData = null; try { oos = new ObjectOutputStream(out); oos.writeObject(user); userData = out.toByteArray(); } catch (IOException e) { e.printStackTrace(); } ObjectInputStream ois = null; try { ois = new ObjectInputStream(new ByteArrayInputStream(userData)); user = (User)ois.readObject(); System.out.println("反序列化后 2: " + user); } catch (Exception e) { e.printStackTrace(); } }
-
实现细节:
-
需要重写writeExternal()与readExternal()并传入指定对象同时需要指定所有的成员变量
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String)in.readObject(); age = in.readInt(); }
- 如果说在writeExternal()方法中少写了类属性但是读取的时候又是全部都读取了的,那么就会抛出异常:java.io.EOFException
- 如果说在这两个方法中,对某一类属性均缺失,那么反序列化得到的对象的这个属性就是零值(对象初始化中的默认值)
- 还有对类属性读取顺序,两个方法中要完全一致,不然要报错
-
需要实现类的无参构造即使实现了其他的构造函数:
-
不然就报错:java.io.InvalidClassException(没有合法的构造函数)
- 为什么?这个在源码中才知道
-
-
-
-
关于序列化的面试题:
-
什么是SerialVersionUID,如果不定义这个,会发生什么?
-
概述:通常是对象的哈希码,可以用工具看;
-
作用:用来做对象的版本控制
-
不定义的话,当修改或者添加类的任何字段,此时已经序列化的对象无法反序列化回去
- 因为此时新类的UID,跟反序列化回来的UID不同
- 抛出:java.io.InvalidClassExption(无效类异常)
-
怎么指定SerialVersionUID?直接显示定义
private static final long serialVersionUID = 2;
-
-
事先定义好了SerialVersionUID,在序列化之后,再次修改SerialVersionUID,发生什么?
-
因为SerialVersionUID不同,反序列化失败,报错,抛出异常
-
使用场景:
- 客户端将服务器端的一个对象缓存起来,每次用的时候就掉缓存,当服务器端需要更新客户端的这个缓存对象;那么,修改SerialVersionUID就行了
-
-
序列化是,希望某些成员不要序列化,如何实现?还可以问trasient瞬态变量拿来干啥子的?
-
不想被序列化,在声明的时候加上trasient就行了(反序列化后,这个字段为null,其实是零值)
public transient String nickName;
-
-
如果类中的一个成员未实现序列化接口,会发生什么情况?
-
对可序列化的对象进行序列化的时候,此时若类包含了不可序列化的对象的引用;
- 在类里面搞一个自定义的类(不实现序列化接口)的对象,那么抛出异常
- String类是实现了序列化接口的
-
-
如果类是可序列化(实现了接口)的,但是父类不是,则反序列化后从父类继承的实例变量的状态如何?
- 就是去看这个父类中的成员变量还有没有值
- 反序列的值是空的;其实可以解决这个问题(父类要实现无参构造,加入方法)
-
是否可以自定义序列化过程,或者是否可以覆盖Java中的默认序列化过程:可以的
-
自定义序列化:借助对象I/O流直接操作对象(private修饰的两个函数合适调用)
private void writeObject(ObjectOutputStream out) throws IOException {//不是重写父类的方案 out.defaultWriteObject(); out.writeObject(getSex()); out.writeInt(getId()); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); setSex((String)in.readObject()); setId(in.readInt()); }
-
-
假设子类的父类实现了可序列化接口,如何避免子类被序列化
-
子类继承了实现序列化接口的父类,那么子类就默认可序列化,
-
不想这样干,实现相应的序列化方法,在调用这个方法的时候就抛异常
-
代码:
private void writeObject(java.io.ObjectOutputStream out) throws IOException { throw new NotSerializableException("Can not serialize this class"); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { throw new NotSerializableException("Can not serialize this class"); } private void readObjectNoData() throws ObjectStreamException { throw new NotSerializableException("Can not serialize this class"); }
-
-
-
java序列化和反序列化过程中使用了哪些方法
-
概述:实现的序列化接口只是一个标识,核心还是在对象I/O流
-
可以对对象I/O流进行封装(内部就可以使用很多其他的东西)
-
-
-
序列化流程(源码)
-
序列化:
-
调度流程:
-
objectOutPutStream 实例.writeObject(待序列化类的实例);
out.writeObject(getSex());
-
进入public final void writeObject(Object obj) 函数-
this.writeObject0(obj, false);
-
进入private void writeObject0(Object obj, boolean unshared)函数
-
参数为基本类型就直接写,是类就走具体的方法
if (obj instanceof String) { this.writeString((String)obj, unshared); return; } else if (cl.isArray()) { this.writeArray(obj, desc, unshared); return; ……
-
细节:对枚举类型由特殊处理,存入枚举类型的name
private void writeEnum(Enum<?> en, ObjectStreamClass desc, boolean unshared) throws IOException { this.bout.writeByte(126); ObjectStreamClass sdesc = desc.getSuperDesc(); this.writeClassDesc(sdesc.forClass() == Enum.class ? desc : sdesc, false); this.handles.assign(unshared ? null : en); this.writeString(en.name(), false); }
-
细节:writeOrdinaryObject(根据对象实现的序列化接口的不同,进行分发)
if (desc.isExternalizable() && !desc.isProxy()) { this.writeExternalData((Externalizable)obj); } else { this.writeSerialData(obj, desc); }
-
细节:writeOrdinaryObject
-
拿到对象描述符(或者地址)去写,
this.bout.writeByte(115); this.writeClassDesc(desc, false); xxx if (desc.isExternalizable() && !desc.isProxy()) { this.writeExternalData((Externalizable)obj); } else { this.writeSerialData(obj, desc); }
-
-
-
进入private void writeClass(Class<?> cl, boolean unshared)函数
- 通过字节形式进行读取,传入了一个对象描述符
this.bout.writeByte(118);//通过字节形式进行读取,传入了一个对象描述符 this.writeClassDesc(ObjectStreamClass.lookup(cl, true), false); this.handles.assign(unshared ? null : cl);
-
进入private void writeClassDesc(ObjectStreamClass desc, boolean unshared)函数
-
开始写入对象
if (!unshared && (handle = this.handles.lookup(desc)) != -1) { this.writeHandle(handle); } else if (desc.isProxy()) { this.writeProxyDesc(desc, unshared); } else { this.writeNonProxyDesc(desc, unshared); }
-
-
进入函数private void writeProxyDesc(ObjectStreamClass desc, boolean unshared)
-
写对象
this.bout.setBlockDataMode(false); this.bout.writeByte(120); this.writeClassDesc(desc.getSuperDesc(), false);
-
-
-
-
序列化中private方法,又不是重写了父类的,这个是在哪里调用的?
-
概述:通过反射调用的
-
证明:有一个hashWriteObjectMethod方法
- 存在ObjectStreamClass类:描述类待序列化类的结构
- 在这个类里面读取类结构,看有没有writeObject方法,有就存起来
- 然后会调用hashWriteObjectMethod,有那么就invoke反射调用这个方法
-
-
为什么每次调用都要求有一个无参构造函数?
- 当去read一个对象的时候,调用readObject(Object.class)
- 调用readObject0(type,unshared:false)
- 调用readOrdinaryObject(unshared)
- 调用newInstance:默认去调用待序列化类的无参构造函数
-
java序列化对枚举类型的处理:
-
概述:
- 在序列化过程中,只存储枚举类型的引用和常量的名称(en.name)
- 在反序列化过程中,在运行时环境中查找已经存在的枚举对象
-
代码检验
-
定义枚举类型
-
打印序列化后与反序列化后的hashCode,发现是一样的(相同的对象引用时完全相同的)为什么会这样呢?
枚举对象的读取流程
- readEnum时,拿到枚举对象的名字
- 调用Enum.valueOf((Class)C1,name)
- 调用enumConstantDirectory().get(name):通过字典直接拿到枚举对象
将枚举对象序列化时的流程
-
在序列化对象(转成二进制),先存储从哪里开始
bout.writeByte(TC_ENUM)//里面是对象标记
-
发现这个对象是枚举类型,那么就只存一个name
writeString(en.name(),false)
-
读取的时候,还是去找枚举字典
-
-
在序列化时使用枚举的好处
- 使用枚举类型单例,可以避免单例在序列化时失效
-
在保证单例序列化与反序列化后,单例仍然存在
-
代码展示:这种是可以通过反射进行破坏的
class SingleTon implements Serializable{ public static SingleTon INSTANCE = new SingleTon(); private Singleton(); private Object readResolve(){ return INSTANCE; } }
-
解决反射破坏问题:使用synchronized(SingleTon.class)包裹构造函数
private SingleTon(){ synchronized(SingleTon.class){ if(flag == false){ flag = true }else{ throw new RuntimeException("破坏单例了") } } }
-
补充:readResolve方法
当调用readOrdinaryObject时
-
检查是否实现了 readResolve方法,最先检测
desc.hasResolveMethod()
-
如果说实现了那就去调用,那么就过滤掉了系统的其他方法
-
-
-
-
流程:
-
示意图:
-
-
-
Parcelable
-
概述:Android特有序列化方案,基于Binder实现进程间通信
-
简单使用:一般是自动生成代码
-
实现Parceable接口
-
重写方法:
- describeContents(一般返回0,特殊处理返回1)
- writeToParcelable(调用write具体类型写进去):如何进行序列化
-
读取的时候需要有构造,需要去new对象
public User(Parcel in){ name = in.readString(); age = in.readInt(); }
-
-
构造中的读取调用流程:
-
readString--->nativeReadString--->framwork层
- write方法会调用到writeAligned(FrameWork层)
- read方法会调用到readAligned(FrameWork层)
-
-
Serializable与Paecelable的区别
-
示意图:
-
为什么是1M,修改内核是4M?
- Binder通信机制规定了,一个进程最多就1M,要是改内核就是4M
-
Serializable 产生内存碎片
- 在创建对象的时候,大量使用了反射操作
-
-
-
面试题
-
反序列化后对象,需要调用构造函数重新构造吗?
- 不会(序列化会调用构造函数),反序列实际上是读取二进制数据,拼接成Object,然后进行强制类型转化
- 为什么会产生内存碎片,为什么效率低:在反序列化时在对成员变量处理的时候,就会调用无参构造函数
-
序列化和反序列化后的对象是什么关系(是==还是equal,是深拷贝还是浅拷贝)
- 就相当于是深拷贝,对象前后的地址是不同的;但是枚举例外
-
Android里面为什么要设计出Bundle而不直接使用Map结构
-
Bundle内部使用了ArrayMap(比HashMap省空间)并且Bundle主要用于传输少量数据
static{ xxx EMPTY = new Bundle(); EMPTY.mMap = ArrayMap.EMPTY(); }
-
-
为何Intent不能在组件之间直接传递对象而要通过序列化机制?
- 这个牵涉到AMS与Bundle
- 因为Activity通信是需要跟AMS进行交互的(静态代理较多),而AMS与APP进程不是同一个进程,所以需要跨进程通信;
-
序列化与持久化的关系与区别
- 序列化:跨进程通信;持久化:存储数据;
-