什么是序列化
-
序列化:将数据结构或对象转换成字节序列的过程。
-
反序列化:将在序列化过程中所生成的字节序列转换成数据结构或者对象的过程。
-
数据结构、对象与二进制串:不同的计算机语言中,数据结构,对象以及二进制串的表示方式并不相同。
- Java 这种完全面向对象的语言,工程师所操作的一切都是对象(Object),来自于类的实例化。在 Java 语言中最接近数据结构的概念,就是 POJO(Plain Old Java Object)或者 Javabean--那些只有 setter/getter 方法的类。
- 而在 C 二进制串:序列化所生成的二进制串指的是存储在内存中的一块数据。
- C 语言的字符串可以直接被传输层使用,因为其本质上就是以'0'结尾的存储在内存中的二进制串。
- 在 Java 语言里面,二进制串的概念容易和 String 混淆。实际上 String 是 Java 的一等公民,是一种特殊对象(Object)。
- 对于跨语言间的通讯,序列化后的数据当然不能是某种语言的特殊数据类型。二进制串在 Java 里面所指的是 byte[],byte 是 Java 的 8 中原生数据类型之一(Primitive data types)。
为什么序列化
那通过上面我们了解到序列化其实就是对象/数据结构和字节序列的转换,那么之所以转换成字节序列是为了进行数据传输和数据持久化。
- 传输:在系统底层,系统只能识别01010的字节序列,那我们为了在某些场景中将对象进行传输就需要先将对象进行序列化。(也就是说,可以在运行 Windows 系统的计算机上创建一个对象,将其序列化,通过网络将它发送给一台运行 Unix 系统的计算机,然后在那里准确地重新组装,而却不必担心数据在不同机器上的表示会不同,也不必关心宇节的顺序或者其他任何细节。)
- 持久化:我们知道在程序运行中,创建完对象,只要你需要它会一直存在,程序终止,对象的声明周期也随之结束。如果在某些场景下程序终止,我们仍需要使用对象做事情,那么就要将对象进行
持久化
,将对象序列化之后的字节序列保存到文件或磁盘。
序列化应用场景
- 进程间数据传输(因为进程间数据不是共享的,如果想要进行数据传输或共享数据,就必须进行序列化)
- 网络传输(如现在开发的API一般返回的都是Json格式,Json也是序列化协议的一种)
- 状态恢复(比如在程序关闭时将对象的信息序列化存储,程序重启后通过反序列化进行一些状态的恢复)
- 永久保存(比如说java中的对象,他是运行时的瞬时对象,如果需要持久化,那么就需要序列化后将它保存到磁盘或数据库中)
- 加密(如果传输的数据需要进行加密,则在将数据进行序列化时进行加密,接收数据后解密,然后反序列化拿到数据)
注意:序列化对象时只是针对对象的变量进行序列化,不会针对方法进行序列化(类结构是静态的,而对象是动态的)
常见的序列化协议
XML&SOAP
-
XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议
-
JSON(Javascript Object Notation)
- JSON 起源于弱类型语言 Javascript, 它的产生来自于一种称之为"Associative array"的概念,其本质是
就是采用"Attribute-value"的方式来描述对象。实际上在 Javascript 和 PHP 等弱类型语言中,类的描
述方式就是 Associative array。JSON 的如下优点,使得它快速成为最广泛使用的序列化协议之一。
- 这种 Associative array 格式非常符合工程师对对象的理解。
- 它保持了 XML 的人眼可读(Human-readable)的优点。
- 相对于 XML 而言,序列化后的数据更加简洁。 来自于的以下链接的研究表明:XML 所产生序列 化之后文件的大小接近 JSON 的两倍
- 它具备 Javascript 的先天性支持,所以被广泛应用于 Web browser 的应用常景中,是 Ajax 的事 实标准协议。
- 与 XML 相比,其协议比较简单,解析速度比较快。(所以在网络传输中用的比较多)
- 松散的 Associative array 使得其具有良好的可扩展性和兼容性
- JSON 起源于弱类型语言 Javascript, 它的产生来自于一种称之为"Associative array"的概念,其本质是
就是采用"Attribute-value"的方式来描述对象。实际上在 Javascript 和 PHP 等弱类型语言中,类的描
述方式就是 Associative array。JSON 的如下优点,使得它快速成为最广泛使用的序列化协议之一。
-
Protobuf(google出的序列化协议,性能非常好)
- Protobuf 具备了优秀的序列化协议的所需的众多典型特征。
- 标准的 IDL 和 IDL 编译器,这使得其对工程师非常友好。
- 序列化数据非常简洁,紧凑,与 XML 相比,其序列化之后的数据量约为 1/3 到 1/10。
- 解析速度非常快,比对应的 XML 快约 20-100 倍。
- 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码
- Protobuf 具备了优秀的序列化协议的所需的众多典型特征。
序列化协议特征
上面也列举了一些常见的序列化协议,在使用时,我们要根据不同的场景和每种序列化协议的特征来进行选择。
通用性
- 技术层面,序列化协议是否支持跨平台、跨语言。如果不支持,在技术层面上的通用性就大大降低(因为在真实的项目中一个项目可能要最终运行到多个平台比如android或ios或者后端,又或者在应用开发中使用的不仅仅是一种开发语言,那么这时候就要考虑序列化的通用性) 了。
- 流行程度,序列化和反序列化需要多方参与,很少人使用的协议往往意味着昂贵的学习成本;另一 方面,流行度低的协议,往往缺乏稳定而成熟的跨语言、跨平台的公共包。
强健性 / 鲁棒性
- 成熟度不够(也就是说一个序列化协议从制定到创建到应用到迭代到成熟的过程是否完整)
- 语言 / 平台的不公平性(因为每个平台对应开发语言的不同导致序列化协议需要做大量的工作去兼容不同的平台)
- 可调试性 / 可读性
- 支持不到位(xml json protobuf想一下那个可读性强?)
- 访问限制
性能:性能包括两个方面,时间复杂度和空间复杂度。
- 空间开销(Verbosity), 序列化需要在原有的数据上加上描述字段,以为反序列化解析之用。如 果序列化过程引入的额外开销过高,可能会导致过大的网络,磁盘等各方面的压力。对于海量分布 式存储系统,数据量往往以 TB 为单位,巨大的的额外空间开销意味着高昂的成本。
- 时间开销(Complexity),复杂的序列化协议会导致较长的解析时间,这可能会使得序列化和反 序列化阶段成为整个系统的瓶颈。
可扩展性 / 兼容性
- 移动互联时代,业务系统需求的更新周期变得更快,新的需求不断涌现,而老的系统还是需要继续维 护。如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大 提供系统的灵活度。
安全性 / 访问限制
- 在序列化选型的过程中,安全性的考虑往往发生在跨局域网访问的场景。当通讯发生在公司之间或者跨
机房的时候,出于安全的考虑,对于跨局域网的访问往往被限制为基于 HTTP/HTTPS 的 80 和 443 端
口。如果使用的序列化协议没有兼容而成熟的 HTTP 传输层框架支持,可能会导致以下三种结果之一:
- 因为访问限制而降低服务可用性;
- 被迫重新实现安全协议而导致实施成本大大提高;
- 开放更多的防火墙端口和协议访问,而牺牲安全性
- 注意点:Android的Parcelable也有安全漏洞
Java中的序列化
上面说了一些序列化的概念以及常见的序列化协议,还有序列化的特征,那在Java中有哪些序列化方案?
1.Serializable
代码实现
Java 提供的序列化接口,它是一个空接口:
public interface Serializable {
}
Serializable
用来标识当前类可以被 ObjectOutputStream
序列化,以及被 ObjectInputStream
反序列化。
声明一个序列化类,继承Serializable
接口,并声明serialVersionUID
。
//
public class Person implements Serializable {
private static final long serialVersionUID = -5945415387430920684L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
调用:
public static void main(String[] args) throws Exception {
Person person = new Person("serial", 18);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(person);
byte[] serialPerson = baos.toByteArray();
System.out.println(Arrays.toString(serialPerson));
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialPerson));
Person person1 = (Person) ois.readObject();
System.out.println(person1.toString());
}
结果:
使用 Serializable 要注意以下几点:
- 使用使用
Serializabl
方式序列化,要在类中添加serialVersionUID
,同来标识唯一(兼容与安全,如果父类声明了,则子类不需要声明)。 - 使用
transient
关键字标识的成员变量(在序列化后,成员变量的值被设置为初始值,如果成员变量是对象则是null) - 静态成员变量属于类不属于对象,所以不会参与序列化(对象序列化保存的是对象的“状态”,也 就是它的成员变量,因此序列化不会关注静态变量)
transient
关键字对静态成员变量无效,因为静态变量不参与序列化,所以他即使被transient
标识,他的值也不会被修改。
serialVersionUID 与兼容性
serialVersionUID
的作用:serialVersionUID
用来表明类的不同版本间的兼容性。如果你修改了此类, 要修改此值。否则以前 用老版本的类序列化的类恢复时会报错:InvalidClassException
- 设置方式:在JDK中,可以利用JDK的bin目录下的
serialver.exe
工具产生这个serialVersionUID
,对于Test.class
,执行命令:serialver Test
- 兼容性问题:为了在反序列化时,确保类版本的兼容性,最好在每个要序列化的类中加入
private static final long serialVersionUID
这个属性,具体数值自己定义。这样,即使某个类在与之对应的对象已经序列化出去后做了修改,该对象依然可以被正确反序列化。否则,如果不显式定义该属性,这个属性值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。不显式定义这个属性值的另一个坏处是,不利于程序在不同的JVM之间的移植。因为不同的编译器实现该属性值的计算策略可能不同,从而造成虽然类没有改变,但是因为JVM不同,出现因类版本不兼容而无法正确反序列化的现象出现. - 因此 JVM 规范强烈 建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同
时最好是
private
和final
的,尽量保证不变。
2.Externalizable
Externalizable
继承自Serializable
,在使用上基本一样,只是需要声明一个空构造方法,和覆写他的writeExternal
和readExternal
方法.
//Externalizable的源码
public interface Externalizable extends Serializable {
void writeExternal(ObjectOutput var1) throws IOException;
void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
}
声明一个类继承Externalizable
,并声明serialVersionUID
,创建一个空构造,并覆写两个方法:
public class Car implements Externalizable {
private static final long serialVersionUID = -3731549185451154398L;
private int num;
private String color;
private float price;
public Car() {
}
public Car(int num, String color, float price) {
this.num = num;
this.color = color;
this.price = price;
}
@Override
public void writeExternal(ObjectOutput objectOutput) throws IOException {
objectOutput.writeInt(num);
objectOutput.writeObject(color);
objectOutput.writeFloat(price);
}
@Override
public void readExternal(ObjectInput objectInput) throws IOException, ClassNotFoundException {
num = objectInput.readInt();
color = (String) objectInput.readObject();
price = objectInput.readFloat();
}
@Override
public String toString() {
return "Car{" +
"num=" + num +
", color='" + color + '\'' +
", price=" + price +
'}';
}
}
使用:
public static void main(String[] args) throws Exception {
Car car = new Car(1,"red", 180000f);
//序列化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(car);
byte[] serialCar = baos.toByteArray();
System.out.println(Arrays.toString(serialCar));
//反序列化
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialCar));
Car car1 = (Car) ois.readObject();
System.out.println(car1.toString());
}
结果:
使用 Externalizable 要注意以下几点:
- 使用
Externalizable
序列化,必须要声明一个无参构造,否则会报no valid constructor
的异常。 - 使用
Externalizable
序列化,相当于所有的成员变量都添加了transient
关键字,如果想要序列化或反序列化哪些字段需要自己覆写writeExternal
和readExternal
方法。 - 使用
Externalizable
序列化,transient
关键字可以使用,但没有任何效果。
Java的序列化工具类
通过上面我们知道了在Java
中的序列化使用,也知道它的序列化和反序列化分别通过ObjectOutputStream
和ObjectInputStream
进行,那我们可以简单的封装一个工具类,用来序列化和反序列化,以及将序列化的二进制进行本地保存和读取。
public class SerializeableUtils {
public static <T> byte[] serialize(T t) throws Exception{
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(t);
return out.toByteArray();
}
public static <T> T deserialize(byte[] bytes)throws Exception{
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
T t = (T)ois.readObject();
return t;
}
/**
* 序列化对象
*
* @param obj
* @param path
* @return
*/
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;
}
/**
* 反序列化对象
*
* @param path
* @param <T>
* @return
*/
@SuppressWarnings("unchecked ")
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的序列化步骤与数据结构分析
序列化算法一般会按步骤做如下事情:
- 将对象实例相关的类元数据输出。
- 递归地输出类的超类描述直到不再有超类。
- 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
- 从上至下递归输出实例的数据
我们使用工具类将之前创建的Person
类序列化到本地打开分析一下。
- AC ED: STREAM_MAGIC. 声明使用了序列化协议.
- 00 05: STREAM_VERSION. 序列化协议版本.
- 0x73: TC_OBJECT. 声明这是一个新的对象.
- 0x72: TC_CLASSDESC. 声明这里开始一个新Class。
- 00 2e: Class名字的长度.
我们看到序列化后的文件结构和class
文件的结构很像,也是有魔数、版本号、变量描述性信息、变量值...,其实序列化就是读取到对象信息后遍历,通过序列化算法按照序列化的结构写入。
Java的序列化readObject/writeObject原理分析
这里通过ObjectOutputStream
的writeObject
进行分析
1. ObjectOutputStream的构造函数设置enableOverride = false
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;//enableOverride = false
...
}
2. 所以writeObject方法执行的是writeObject0(obj, false);
这里我们发现如果enableOverride = true
则走writeObjectOverride(obj)
,否则调用writeObject0(obj, false)
,查看writeObjectOverride(obj)
方法,我们发现实一个空实现并抛出异常的方法,它的原理就是要我们自己继承ObjectOutputStream
,覆写这个方法来实现对象的写入操作,如果不去覆写这个writeObjectOverride(obj)
,它会在内部抛出异常。
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
//do sth for error....
}
}
protected void writeObjectOverride(Object obj) throws IOException {
// BEGIN Android-added: Let writeObjectOverride throw IOException if !enableOverride.
if (!enableOverride) {
// Subclasses must override.
throw new IOException();
}
// END Android-added: Let writeObjectOverride throw IOException if !enableOverride.
}
3. 在writeObject0方法中,代码非常多,看重点
private void writeObject0(Object obj, boolean unshared)throws IOException{
//..............
// remaining cases
// BEGIN Android-changed: Make Class and ObjectStreamClass replaceable.
if (obj instanceof Class) {
writeClass((Class) obj, unshared);
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
// END Android-changed: Make Class and ObjectStreamClass replaceable.
} else if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
//看这里,判断实现Serializable接口,走这个方法
writeOrdinaryObject(obj, desc, unshared);
} else {
//如果没有实现Serializable接口,会报NotSerializableException
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
4. 在writeOrdinaryObject(obj, desc, unshared)方法中
private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared)
...
if (desc.isExternalizable() && !desc.isProxy()) {
//如果对象实现了Externalizable接口,那么执行writeExternalData((Externalizable) obj)方法
writeExternalData((Externalizable) obj);
} else {
//如果对象实现的是Serializable接口,那么执行的是writeSerialData(obj, desc)
writeSerialData(obj, desc);
}
...
}
//这里我们看看writeExternalData
5. writeExternalData((Externalizable) obj)方法中
我们先看下实现了Externalizable
接口是如何序列化的,它其实最终调用到我们实现Externalizable
后覆写的writeExternal()
方法
private void writeExternalData(Externalizable obj) throws IOException {
do sth for check...
try {
curContext = null;
if (protocol == PROTOCOL_VERSION_1) {
//这里其实就是调用到我们实现Externalizable后覆写的writeExternal()方法
obj.writeExternal(this);
} else {
bout.setBlockDataMode(true);
//这里其实就是调用到我们实现Externalizable后覆写的writeExternal()方法
obj.writeExternal(this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
}
} finally {
do sth...
}
}
6. writeSerialData方法,主要执行方法:defaultWriteFields(obj, slotDesc)
我们再看一下writeSerialData()
方法,其实defaultWriteFields(obj, slotDesc)
就是真实序列化过程的方法,但在这之前会有一个判断,它会通过hasWriteObjectMethod()
反射去判断实现Serializable
序列化类是否包含指定的一些方法(可看下面的源码中的判断),如果包含这些方法的话,回去调用你覆写的序列化方法。
那这样设计有什么应用场景?我们一会再说。
/**
* Writes instance data for each serializable class of given object,from
* superclass to subclass.
* 最终写序列化的方法
*/
private void writeSerialData(Object obj, ObjectStreamClass desc)throws IOException{
...
if (slotDesc.hasWriteObjectMethod()) {
//如果writeObjectMethod != null(目标类中定义了私有的writeObject方法),那么将调用目标类中的writeObject方法
...
slotDesc.invokeWriteObject(obj, this);
...
} else {
//如果如果writeObjectMethod == null, 那么将调用默认的defaultWriteFields方法来读取目标类中的属性
defaultWriteFields(obj, slotDesc);
}
}
hasWriteObjectMethod()
方法通过反射判断是否存在writeObject
、readObject
、readObjectNoData
、writeReplace
、readResolve
方法
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
}
writeReplaceMethod = getInheritableMethod(
cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
return null;
7. 反射判断机制有什么应用场景
我们知道Java
中的ArrayList
是实现了Serializable
接口的但是我们查看源码发现他的数据变量elementData
,被transient
关键字标识为不进行序列化的字段,那他是如何将数据序列化的呢?
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
...
transient Object[] elementData;
...
}
那我们通过在源码中查找发现了它写了一个方法,注意这个不是覆写,就仅仅是声明了一个方法writeObject()
。
那么在序列化时,API通过hasWriteObjectMethod()
方法,发现在Arraylsit
中存在这样的方法,那么就会调用Arraylsit
自己的writeObject()
去进行序列化。
我们知道Object[] elementData
,数组的长度是固定的,那可能会出现空数据,而ArrayList
通过var1.writeInt(this.size);
和var3 < this.size
这行代码,只序列化有效的数据,减小的序列化后的数据大小。
private void writeObject(ObjectOutputStream var1) throws IOException {
int var2 = this.modCount;
var1.defaultWriteObject();
//这句很重要
var1.writeInt(this.size);
//这句很重要
for(int var3 = 0; var3 < this.size; ++var3) {
var1.writeObject(this.elementData[var3]);
}
if (this.modCount != var2) {
throw new ConcurrentModificationException();
}
}
所以,通过了解源码,我们得知,如果对序列化的数据有单独的要求,除了继承Externalizable
,也可以通过这种方式去实现序列化/反序列化,进而来保证序列化的过程由自己操作。
Java序列化中的坑
多引用写入
我们还是用之前的Person
演示
public static void main(String[] args) throws Exception {
Person person = new Person("Serializeable",18);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
//序列化person
oos.writeObject(person);
//修改person的name和age
person.setName("Externalizable");
person.setAge(19);
oos.writeObject(person);
oos.close();
byte [] b = bos.toByteArray();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(b));
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
System.out.println("Person1 = "+p1.toString());
System.out.println("Person2 = "+p2.toString());
}
打印结果:
Person1 = Person{name='Serializeable', age=18}
Person2 = Person{name='Serializeable', age=18}
原因:
- 在默认情况下, 对于一个实例的多个引用,为了节省空间,只会写入一次,后面会追加几个字节代表某个实例的引用。
解决:有两种解决方法
- 1.修改对象变量后,不使用
oos.writeObject(person);
而使用oos.writeUnshared(person);
(对于API官方是这样解释的:通过writeUnshared
写入的对象总是以与新出现的对象(尚未写入流的对象)相同的方式序列化,无论该对象是否先前已被写入。) - 2.修改对象变量后,仍然使用
oos.writeObject(person);
,但在这之前要调用下oos.reset();
重置流。
解决后的输出结果:
Person1 = Person{name='Serializeable', age=18}
Person2 = Person{name='Externalizable', age=19}
子类实现序列化,父类不实现序列化/ 对象引用
- 我们知道如果一个类实现了
Serilizable
接口,那么它的子类自然也实现了它,所以也可以序列化,那如果是子类实现了Serilizable
接口,而父类却没有,我们在序列化的时候,是可以序列化的,但是反序列化的时候就会抛出java.io.NotSerializableException
异常,而解决的方法,一种办法是让父类也实现Serilizable
,另一种办法是在没有实现Serilizable
的父类中声明一个无参构造。
类的演化
增加和修改成员变量
- 这个的意思是,我们声明一个需要序列化的类,创建对象进行序列化到本地,然后将类中的变量增加/修改,再反序列化原来存储的文件,则会出现的一些问题。
- 增加:如果增加了成员变量,则不会报错,且反序列化后新增的变量值为默认值。
- 修改:如果对原来成员变量的类型进行修改,则反序列化会报错
incompatible types for field name
。
枚举
- 如果对一个声明了枚举的类进行序列化,然后对其枚举的顺序进行调整,反序列化后,枚举的值会变。
- 事实上序列化Enum对象时,并不会保存元素的值,只会保存元素的name。这样,在不依赖元素值的前提下,ENUM对象如何更改都会保持兼容性。
注意
- 所以一般序列化化类修改后我们也要对
serialVersionUID
进行修改,这样就可以保证序列化和反序列化的唯一性和安全性。
单例模式的序列化问题/反射问题
在某些情况下我们需要对单例的对象进行序列化,但是序列化会破坏单例。如下:
public class PersonUtils implements Serializable {
private static PersonUtils mInstance;
public static PersonUtils getInstance() {
if (mInstance == null) {
synchronized (PersonUtils.class) {
if (mInstance == null) {
mInstance = new PersonUtils();
}
}
}
return mInstance;
}
//如果不重写readResolve,会导致单例模式在序列化->反序列化后失败
// private Object readResolve() {
// return mInstance;
// }
}
调用:
public static void main(String[] args) throws Exception {
String path = System.getProperty("user.dir") + "a.txt";
FileOutputStream fos = new FileOutputStream(path);
ObjectOutputStream oos = new ObjectOutputStream(fos);
//通过单例获取 personUtils1
PersonUtils personUtils1 = PersonUtils.getInstance();
//序列化保存到本地
oos.writeObject(personUtils1);
oos.close();
FileInputStream fis = new FileInputStream(path);
ObjectInputStream ois = new ObjectInputStream(fis);
//通过反序列化获取 personUtils2
PersonUtils personUtils2 = (PersonUtils) ois.readObject();
//通过单例获取 personUtils3
PersonUtils personUtils3 = PersonUtils.getInstance();
System.out.println("通过单例获取:personUtils1 = " + personUtils1.hashCode());
System.out.println("通过序列化获取:personUtils2 = " + personUtils2.hashCode());
System.out.println("通过单例获取:personUtils3 = " + personUtils3.hashCode());
}
结果:
通过单例获取:personUtils1 = 2101973421
通过序列化获取:personUtils2 = 1149319664
通过单例获取:personUtils3 = 2101973421
解决方案:在单例中添加以下方法并返回单例实例。
private Object readResolve() {
return mInstance;
}
- 注意:反射也会破坏单例,虽然可以通过添加flag去做一些判断,但反射仍然可以创建单例类的实例,因为反射可以拿到和修改一个类的所有信息。
Android中的序列化
Parcelable概念
- 通过对
Java
中序列化方案的使用以及源码的阅读,我们发现其中大量使用到反射,我们知道反射会降低程序的性能,会创建了许多中间对象,导致内存碎片,频繁gc,这也是使用Serializable
序列化的缺点,而Android
又是基于Java
开发的,所以Google
又提供了基于Android
的序列化方案Pracelable
。 Parcelable
是Android SDK
提供的,它是基于内存的,由于内存读写速度高于硬盘,因此Android
中的跨进程对象的传递一般使用Parcelable
。- 至于
Serializable
和Parcelable
他们哪个性能更高些,可以将结构相同的两个类实现不同的序列化接口,在两个Activity中序列化和反序列化多次进行比较,但我认为这样验证的意义不是很大,因为在数据量小和测试机不同的的情况下,数据结果会有一些不同,而如果传递数据量大的时候,Bundle也不支持会挂掉。然而我还看到过这样一篇文章:别再说Serializable性能不如Parcelable啦,文章大致意思是Parcelable
比Serializable
快,是因为对比的是自动序列化的Serializable
,而Serializable
如果声明设置了readObject
和writeObject
之后就很快了, 而且传输的体积要小。这里我就不再去测试了有兴趣的自己写编写测试代码进行对比测试。
Parcelable原理
- Parcel翻译过来是打包的意思,其实就是包装了我们需要传输的数据,然后在Binder中传输,也就是用于跨进程传输数据。简单来说,Parcel提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象,下图是这个过程的模型。
- Parcel可以包含原始数据类型(用各种对应的方法写入,比如writeInt(),writeFloat()等),可以包含 Parcelable对象,它还包含了一个活动的IBinder对象的引用,这个引用导致另一端接收到一个指向这个 IBinder的代理IBinder。 Parcelable通过Parcel实现了read和write的方法,从而实现序列化和反序列化。
Parcelable源码分析
面试题
1.在Java序列化中如何部分属性序列化?
- 使用transient关键字
- 添加writeObject和readObject方法
- 使用Externalizable实现
2.Serializable和Parcelable性能分析和如何选择使用?
Serializable性能分析
Serializable是Java中的序列化接口,其使用起来简单但开销较大(因为Serializable在序列化过程中使用了反射机制,故而会产生大量的临时变量,从而导致频繁的GC),并且在读写数据过程中,它是通过IO流的形式将数据写入到硬盘或者传输到网络上。
Parcelable性能分析
Parcelable则是以IBinder作为信息载体,在内存上开销比较小,因此在内存之间进行数据传递时,推荐使用Parcelable,而Parcelable对数据进行持久化或者网络传输时操作复杂,一般这个时候推荐使用Serializable。
两种如何选择
- 在使用内存方面,Parcelable比Serializable性能高,所以推荐使用Parcelable。
- Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
- Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable不能很好的保证数据的持续性,在外界有变化的情况下,建议使用Serializable
3.如果非要把Parcelable保存到本地怎么办呢?
4.android中还有哪些序列化方案?
从广义上讲,SQLite
与 SharedPreferences
也属于将数据序列化到文件的方案。
- SQLite主要用于存储复杂的关系型数据,Android支持原生支持SQLite数据库相关操作(SQLiteOpenHelper),不过由于原生API接口并不友好,所以产生了不少封装了SQLite的ORM框架。
- SharedPreferences是Android平台上提供的一个轻量级存储API,一般用于存储常用的配置信息,其本质是一个键值对存储,支持常用的数据类型如boolean、float、int、long以及String的存储和读取。
Android里面为什么要设计出Bundle而不是直接用Map结构
- Bundle内部是由ArrayMap实现的,ArrayMap的内部实现是两个数组,一个int数组是存储对象数据对应下标,一个对象数组保存key和value,内部使用二分法对key进行排序,所以在添加、删除、查找数据的时候,都会使用二分法查找,只适合于小数据量操作,如果在数据量比较大的情况下,那么它的性能将退化。而HashMap内部则是数组+链表结构,所以在数据量较少的时候,HashMap的EntryArray比ArrayMap占用更多的内存。因为使用Bundle的场景大多数为小数据量,我没见过在两个Activity之间传递10个以上数据的场景,所以相比之下,在这种情况下使用ArrayMap保存数据,在操作速度和内存占用上都具有优势,因此使用Bundle来传递数据,可以保证更快的速度和更少的内存占用。
- 另外一个原因,则是在Android中如果使用Intent来携带数据的话,需要数据是基本类型或者是可序列化类型,HashMap使用Serializable进行序列化,而Bundle则是使用Parcelable进行序列化。而在Android平台中,更推荐使用Parcelable实现序列化,虽然写法复杂,但是开销更小,所以为了更加快速的进行数据的序列化和反序列化,系统封装了Bundle类,方便我们进行数据的传输。
Android中Intent/Bundle的通信原理及大小限制
- Intent 中的 Bundle 是使用 Binder 机制进行数据传送的。能使用的 Binder 的缓冲区是有大小限制的(有些手机是 2M),而一个进程默认有 16 个 Binder 线程,所以一个线程能占用的缓冲区就更小了(有人以前做过测试,大约一个线程可以占用 128 KB)。所以当你看到 The Binder transaction failed because it was too large 这类 TransactionTooLargeException 异常时,你应该知道怎么解决了。
为何Intent不能直接在组件间传递对象而要通过序列化机制?
- Intent在启动其他组件时,会离开当前应用程序进程,进入ActivityManagerService进程(intent.prepareToLeaveProcess()),这也就意味着,Intent所携带的数据要能够在不同进程间传输。首先我们知道,Android是基于Linux系统,不同进程之间的java对象是无法传输,所以我们此处要对对象进行序列化,从而实现对象在 应用程序进程 和 ActivityManagerService进程 之间传输。
- 而Parcel或者Serializable都可以将对象序列化,其中,Serializable使用方便,但性能不如Parcel容器,后者也是Android系统专门推出的用于进程间通信等的接口。