序列化 (Serialization)是将对象的状态信息转换为能够存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。之后,能够经过从存储区中读取或反序列化对象的状态,从新建立该对象。——百度百科。java
在Android中序列化最多见的使用场景就是缓存数据了。如今的App中基本须要缓存数据,例如缓存用户登陆信息。git
// 用来保存用户信息
public class User {
private String name;
private int age;
// getter/setter
}
// 用户信息
User user = new User("Eon Liu", 18);
ObjectOutputStream oos = null;
try {
// 缓存路径(须要开启存储权限)
File cache = new File(Environment.getExternalStorageDirectory(), "cache.txt");
oos = new ObjectOutputStream(new FileOutputStream(cache));
// 将用户信息写到本地文件中
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭流
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
一般在登陆成功以后咱们将用户的信息解析成一个相似User
的对象,而后将其保存在SDCard
中。这一过程就须要用到序列化。上面代码咱们并无对User
进行可序列化的处理,因此在保存过程当中就会抛出java.io.NotSerializableException: com.eonliu.sample.serialization.User
这样的Java异常。由于在writeObject
方法中对须要存储的类进行了校验,若是没有实现Serializable
接口就会抛出这个异常信息。处理这种异常也很简单,只要使User
类实现Serializable
接口就能够了。github
Serializable是Java中提供的序列化接口。缓存
package java.io;
public interface Serializable {
}
复制代码
Serializable
是一个空接口,它仅仅是用来标识一个对象是可序列化的。安全
若是想要使User
可被序列化只要实现Serializable
接口便可。ide
public class User implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
// getter/setter
}
复制代码
能够看到User类实现了Serializable
接口,这时User
就能够被序列化了。而且还多了一个serialVersionUID
字段。那么这个字段是干什么用的呢?函数
serialVersionUID
是用来标记User
类版本用的。其声明的格式是任意访问权限修饰符 static final long serialVersionUID = longValue;
由于其做用是标识每一个类的版本,因此最好使用private
控制serialVersionUID
的访问权限仅在当前类有用,不会被其余子类继承使用。工具
若是不显示声明serialVersionUID
那么JVM会根据类的信息生成一个版本号,因为不一样的JVM生成的版本号的能不一致,类的结构也可能发生变化等这些因素均可能致使序列化时候的版本号和反序列化时的版本号不止一次致使运行时抛出InvalidClassException异常
。因此最佳实践仍是在序列化时显示的指定serialVersionUID
字段。其值是一个long
类型的数值。这个值在Android Studio
中默认是不能自动生成的,能够打开Perferences-Editor-Code Style-Inspections-Serialization issues-Serializable class without serialVersionUID
,这样在实现Serializable
接口是若是没有声明serialVersionUID
字段编译器就会给出警告⚠️,根据警告提示就能够自动生成serialVersionUID
字段了。this
总结:加密
serialVersionUID
字段。private
修饰serialVersionUID
字段。Android Studio
或者其余工具生成serialVersionUID
的值。serialVersionUID
值尽可能保持一致,不要随意修改,不然反序列化时会抛出InvalidClassException
异常,反序列化失败。有时候可能要序列化的对象中存在某些字段不须要被序列化。例如用户密码,为了保证安全咱们不须要将密码字段进行序列化,那如何能作到这一点呢?实现Serializable
接口时静态变量(被static
修饰的变量)不会被序列化、另外被transient
关键字修饰的变量也是不会被序列化的。
public class User implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
// getter/setter
}
复制代码
由于静态变量不能被序列化,因此serialVersionUID
须要声明为static
的,另外password
被声明为transient
也不会被序列化。
静态成员返回序列化时会取内存中的值,被transient
修饰的成员变量使用其类型的默认值,例如password
的默认值则为null
。
public class Person {
private boolean sex;
// getter/setter
}
public class User extends Person implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
// getter/setter
}
复制代码
父类Person
没有实现Serializable
接口,单其子类实现了Serializable
接口,因此父类的信息不回被序列化,当咱们保存User
信息时,父类的sex
字段是不会被保存的。反序列化时sex
会使用boolean
类型的默认值false
。
另外当父类没有实现Serializable
接口时,必须有一个可用的无参数构造函数,例如上面的Person
代码并无显示声明构造,JVM会生成一个无参数构造函数,可是若是咱们将其代码改为以下形式:
public class Person {
private boolean sex;
public Person(boolean sex) {
this.sex = sex;
}
// getter/setter
}
复制代码
这里显示声明了Person
的构造函数,其参数为sex
,这也是Person
的惟一构造函数了。由于根据Java机制,当显示声明构造函数时JVM就不会生成无参数的构造函数。这样就会致使反序列化时候没法构造Person
对象,抛出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
异常。
咱们对上面的代码稍做修改。
当父类实现了Serializable
接口时,其子类也能够被序列化。
public class Person implements Serializable {
private static final long serialVersionUID = 2622760185052917383L;
private boolean sex;
// getter/setter
}
public class User extends Person {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
// getter/setter
}
复制代码
当父类Person
实现了Serializable
接口时,则子类User
也能够被序列化。这时sex
、name
、age
这三个字段都会被序列化。
还有一种状况就是当咱们序列化的类中有一个成员变量是一个自定义类的情形。
public class Car {
private String product;
// getter/setter
}
public class Person implements Serializable {
private static final long serialVersionUID = 2622760185052917383L;
private boolean sex;
// getter/setter
}
public class User extends Person {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
private transient String password;
private Car car;
// getter/setter
}
复制代码
在User
中有一个成员变量为Car
类型,由于Car
没有实现Serializable
接口,因此会致使User
序列化失败,抛出java.io.NotSerializableException: com.eonliu.sample.serialization.Car
异常,这时解决办法有两个,一个是使用transient
修饰Car
字段,使其在序列化时被忽略。另外一个办法就是Car
实现Serializable
接口,使其拥有可序列化功能。
总结:
继承关系中,父类实现Serializable
接口,则父类和子类均可被序列化。
集成关系中,父类没有实现Serializable
接口,则父类信息不会被序列化,子类实现Serializable
接口则只会序列化子类信息。
若是被序列化的类中有Class类型的字段则这个Class须要实现Serializable
接口,不然序列化时候回抛出``java.io.NotSerializableException异常。或者使用
transient`将其标记为不须要被序列化。
若是父类没有实现Serializable
接口,则必需要有一个可用的无参数构造函数。不然抛出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
异常。
Serializable
接口预留了几个方法能够用来实现自定义序列化过程。
private void writeObject(java.io.ObjectOutputStream out)throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException 复制代码
上面五个方法就是Java序列化机制中能够用来干预序列化过程的五个方法,他们具体能感谢什么继续往下看。
writeObject
和readObject
这两个方法从名字能够看出来,就是用来读写对象的,在序列化过程当中咱们须要把对象信息经过ObjectOutputStream
保存在存储介质上,反序列化的时候就是经过ObjectInputStream
从存储介质上将对象信息读取出来,而后在内存中生成一个新的对象。这两个方法就能够用来定义这一过程。
// 序列化
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
// 写入性别信息(sex是Person的字段信息)
out.writeBoolean(isSex());
// 写入年龄信息
out.writeInt(age);
}
// 反序列化
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
// 恢复性别信息
setSex(in.readBoolean());
// 恢复年龄信息
age = in.readInt();
}
复制代码
首先这两个方法要成对出现,不然一个都不要写。在readObject
中read的次序要与在writeObject
中write的次序保持一致,不然可能会致使反序列化的数据出现混乱的现象。另外咱们这两个方法不关心父类是否实现了Serializable
接口,如上面代码所示,out.writeBoolean(isSex());
中的sex
字段就是来自父类Person
的,即便Person
没有实现Serializable
接口这个序列化也会正常运行。
若是不须要自定义过程可使用out.defaultWriteObject();
来实现默认的序列化过程,使用in.defaultReadObject();
实现默认的反序列化过程。
重写这两个方法能够自定义序列化和反序列的过程、例如能够本身定义那些字段能够序列化,哪些不被序列化,也能够对字段进行加密、解密的操做等。若是使用默认的序列化、反序列化的过程咱们也能够在其过程的先后插入其余的逻辑代码来完成其余的任务。
readObjectNoData
主要是用来处理当类发生结构性的变化时处理数据初始化的,这么说可能有点抽象,咱们还拿上面的案例来讲明。
public class User implements Serializable {
private static final String TAG = "SerializationActivity";
private static final long serialVersionUID = -5795919384959747554L;
private String name;
private int age;
private transient String password;
private Car car;
// getter/setter
}
复制代码
初版本User
类如上所示,这时候序列化User
对象将其保存在SDCard
上了,而后发现User
取消性别字段,没法知足需求,因而就有了下一版。
public class Person implements Serializable {
private static final long serialVersionUID = -3824243371733653209L;
private boolean sex;
...
}
public class User extends Person implements Serializable {
...
}
复制代码
在第二版本中User
类继承了Person
,同时也有用了性别的属性。此时User
相对于初版本中缓存的数据发生告终构性的变化,当使用第二版的User
反序列化初版的User
信息时父类Person
中的sex
就没办法初始化了,只能使用boolean
类型的默认值,也就是false
了。那如何才能在反序列化过程当中修改sex
的值呢?就能够经过readObjectNoData
方法来完成。
当反序列化过程当中类发生告终构性的变化时readObjectNoData
方法就会被调用,解决上面的问题咱们就能够在Person
中重写readObjectNoData
方法来对sex
进行初始化操做。
private void readObjectNoData() throws ObjectStreamException {
sex = true;
}
复制代码
writeReplace
方法会在writeObject
方法以前被调用,它返回一个Object
,用来替换当前须要序列化的对象,而且在其内部能够用this
来调用当前对象的信息。
// 返回值Object则是真正被序列化的对象
private Object writeReplace() throws ObjectStreamException {
// 新建立一个User对象
User user = new User();
// 新User的name为当前对象的name值
user.name = this.name;
// 新User的age为20
user.age = 20;
// 返回新User对象
return user;
}
复制代码
上面重写了writeReplace
方法,并新建一个User
对象,其name
赋值为当前对象的name
,this
即表示当前对象。其age
赋值为20,而后返回新的user
对象,以后writeObject
方法就会被调用,将在writeReplace
方法中返回的user
对象进行序列化。在反序列化中的获得user
信息与writeReplace
方法中新建的user
信息一致。
在writeReplace
方法中咱们能够对其对象信息作一些过滤或者添加,甚至能够返回其余类型的对象都是能够的。只不过反序列化的过程也要作响应的转换。
readResolve
方法会在readObject
方法以后调用,返回值也是Object
,它表示反序列化最终的对象。在其方法内部可使用this
表示最终反序列化对象。
private Object readResolve() throws ObjectStreamException {
User user = new User();
user.name = this.name;
user.age = 20;
return user;
}
复制代码
这里的实现代码与writeReplace
方式一致,也很好理解,就不过多解释了。了解其运行机制以后至于怎么用你们就能够脑洞大开了。
在上面了解到writeReplace
和readResolve
的访问修饰符为ANY-ACCESS-MODIFIER
,及表明着能够是任意类型的权限修饰符,例如private
、protected
、public
。可是由于这两个方法主要的做用是用来处理当前类对象的序列化与反序列化,因此一般推荐使用private
修饰,以防止其子类重写。
Externalizable
是Java提供的一个Serializable
接口扩展的接口。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
复制代码
使用也很简单,与Serializable
相似。
public class User implements Externalizable {
private static final long serialVersionUID = -5795919384959747554L;
private String name;
private int age;
...
@Override
public void writeExternal(ObjectOutput out) throws IOException {
Log.d(TAG, "writeExternal: ");
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
Log.d(TAG, "readExternal: ");
age = in.read();
}
}
复制代码
与Serializable
的去别就是实现Externalizable
接口必须重启writeExternal
和readExternal
两个方法,其功能就是实现序列化和反序列化的过程。与Serializable
中的writeObject
和readObject
功能同样。另外使用Externalizable
实现序列化须要提供一个public
的无参构造函数,不然在反序列化的过程当中抛出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor
异常。
Serializable
和Externalizable
均可以实现序列化,那么他们有什么区别呢?该如何选择呢?
Serializable
只是标记接口,其序列化过程都交给了JVM处理,使用相比Externalizable
更简单。Externalizable
并非标记接口,实现它就必须重写两个方法来实现序列化和反序列化,相对复杂一点。Serializable
把序列化和反序列化的过程都交给了JVM,因此在个别状况可能其效率不如Externalizable
。因此一般状况下使用Serializable
来实现序列化和反序列化过程便可。只有充分的了解到使用Externalizable
实现其序列化和反序列化会使其效率有所提高才或者须要彻底自定义序列化和反序列化过程才考虑使用Externalizable
。
邮箱:eonliu1024@gmail.com
Github: github.com/Eon-Liu
CSDN:blog.csdn.net/EonLiu