简介
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。——百度百科。
在Android中序列化最常见的使用场景就是缓存数据了。现在的App中基本需要缓存数据,例如缓存用户登录信息。
// 用来保存用户信息
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
接口就可以了。
Serializable
Serializable是Java中提供的序列化接口。
package java.io;
public interface Serializable {
}
Serializable
是一个空接口,它仅仅是用来标识一个对象是可序列化的。
如果想要使User
可被序列化只要实现Serializable
接口即可。
public class User implements Serializable {
private static final long serialVersionUID = 8279379322154244252L;
private String name;
private int age;
// getter/setter
}
可以看到User类实现了Serializable
接口,这时User
就可以被序列化了。并且还多了一个serialVersionUID
字段。那么这个字段是干什么用的呢?
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
字段了。
总结:
- 尽量显示声明
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
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
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
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
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
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 vs Externalizable
Serializable
和Externalizable
都可以实现序列化,那么他们有什么区别呢?该如何选择呢?
-
Serializable
只是标记接口,其序列化过程都交给了JVM处理,使用相比Externalizable
更简单。 -
Externalizable
并不是标记接口,实现它就必须重写两个方法来实现序列化和反序列化,相对复杂一点。 - 由于
Serializable
把序列化和反序列化的过程都交给了JVM,所以在个别情况可能其效率不如Externalizable
。
所以通常情况下使用Serializable
来实现序列化和反序列化过程即可。只有充分的了解到使用Externalizable
实现其序列化和反序列化会使其效率有所提升才或者需要完全自定义序列化和反序列化过程才考虑使用Externalizable
。
Github: https://github.com/Eon-Liu
CSDN:https://blog.csdn.net/EonLiu