1. 什么是序列化和反序列化,首先看一下维基百科的定义:
在数据储存与传送的部分是指将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等,或者透过网络传送资料时进行编码的过程,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。程序被应用在不同应用程序之间传送对象,以及将对象储存到档案或数据库。相反的过程又称为反序列化。
简单的来说就是将java对象转变成字节序列或者xml格式的数据,进而来进行网络传输或者存储到文件中(也可以在内存中传输),从而达到持久化的目的(对象只有在jvm运行时才有,序列化就是考虑到jvm不在运行时也能保存对象)。之所以还有xml格式的序列化数据是因为考虑要兼容不同平台和语言(不同平台和语言都认识xml格式的文件);本篇博客我们只考虑在java和android中的序列化,所以只探讨java对象的字节序列化与反序列化。
2 举个栗子:
首先写一个实体类,实现了Serializable接口(java中,只要一个类实现了该接口,就可以被序列化),该接口不含任何方法和字段:
public class User implements Serializable {
private String name;
private int age;
private Gender gender;
public User(String name, String password, int age, Gender gender) {
this.name = name;
this.password = password;
this.age = age;
this.gender = gender;
System.out.println("有参数构造函数");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
@Override
public String toString() {
return "{ name: "+name+" passwd: "+password+" age: "+age+" gender: "+gender+" }";
}}
其中Gender是枚举类型,因为枚举是继承类java.lang.Enum,该类实现了Serialiazble接口,所以枚举也可以被序列化;
public enum Gender {
Male("男"),Female("女");
private String string;
Gender(String sex){
string = sex;
}
@Override
public String toString() {
return super.toString()+"("+string+")";
}
}
接下来,我们编写一个场景类,用来将User对象保存到user.txt中,然后再从中读取出来,并打印看看。场景类如下:
public class SerializableTest {
public static void main(String[] args) throws IOException,ClassNotFoundException{
File file = new File("user.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
User user = new User("Mike","123456",18,Gender.Male);
objectOutputStream.writeObject(user);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
User newUser = (User) objectInputStream.readObject();
System.out.println(newUser);
System.out.println(user == newUser);
objectInputStream.close();
}
}
打印结果如下:
有参数构造函数
{ name: Mike passwd: 123456 age: 18 gender: Male(男) }
False
可以看到,User对象被顺利序列化了,并且也被顺利反序列化了,但是user和newUser两个对象并不是同一个值,只是他们的内容相同,这点要注意,此外,在反序列化时,并没有调用User类的构造函数。
3 Serialiazble作用
为啥实现了Serializable接口就可以被序列化呢,点击objectOutputStream.writeObject(user),我们看一下源码
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
/* ----- BEGIN android -----
writeFatalException(ex);*/
try {
writeFatalException(ex);
} catch (IOException ex2) {
}
throw ex;
}
}
然后接着点击writeObject0(obj, false);
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
//省略部分代码…
if (obj instanceof Class) {
writeClass((Class) obj, unshared);
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
// ----- END android -----
} 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) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
可以看到,如果对象是Class类型,String类型ObjectStreamClass类型,数组,枚举或者Serizable类型的话,就进行对象的序列化写入工作;当然,Class、String和ObjectStreamClass等类型也实现了Serizable接口,之所以还要分别判断一下,是因为他们的序列化写入方式不同,要分别处理,这点我们从源码也能看出来。
4 transient
假如说User类中有个字段我不想序列化,比如那个password字段,他保存的是用户的密码,这是敏感数据,如果序列化之后,万一泄漏了怎么办,这不要出事了,那我们怎么办呢,其实很简单,只需要在这个字段之前加transient关键字就可以了:
private transient String password;
那我们再来运行一下我们的场景类:打印如下:
{ name: Mike passwd: null age: 18 gender: Male(男) }
Passwd字段为null了,说明没有序列化进来。
5 Externalizable接口
Java还提供了一个Externalizable接口,让我们随意序列化对象,就是我们想序列那个字段就序列哪个字段,灵活度很高,该接口继承Serializable接口,并新增两个方法;
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
现在让我们的User类实现Externalizable接口,当然同时要覆写上面两个方法,
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeBytes(name);
out.writeObject(gender);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readLine();
gender = (Gender) in.readObject();
}
同时,我们在User类中添加一个无参构造函数,否则编译不通过。
public User() {
System.out.println(“无参数构造函数”);
}
打印结果如下:
有参数构造函数
无参数构造函数
{ name: Mike passwd: null age: 0 gender: Male(男) }
False
可以看到,这种方式的反序列化中调用了无参的构造函数。自由度很高,想保持哪个字段就在上面两个方法里写。
6 writeObject()和readObject()
如果我们还是实现Serializable接口,也可以达到上面那种自由序列化的效果吗,答案是肯定,我们还让User实现Serializable,然后在User中添加两个方法,
private void writeObject(ObjectOutputStream out)throws IOException{
//out.defaultWriteObject();//这句意思是按默认方法写,
out.writeBytes(name);
out.writeObject(gender);
}
private void readObject(ObjectInputStream in)throws IOException,ClassNotFoundException{
//in.defaultReadObject();//这句意思是按默认方式读
name = in.readLine();
gender = (Gender) in.readObject();
}
7 serialVersionUID
一般在实体类中,我们最好再定义一个serialVersionUID字段,值可以随便写一个,比如:
private static final long serialVersionUID = 1L;
写这句有啥用呢,我举个例子,比如,我们的User类,之前已经被序列化到文件里,现在由于需求变更,需要在User类中再加一个字段userId,那么,加好之后,此时再去反序列化之前已经保存在文件里的对象,会抛出异常,就是因为java检测到现在的serialVersionUID与保存在文件里的serialVersionUID不一致,由于我们之前没有手动定义serialVersionUID这个字段,所以这个字段是java自己算的一个值,每次增加或者删除字段,这个值都会变化,如果,我们手动写死的话,这个值就不变了,也不会报异常了,这个读者可以自己实践一下。
至此,java的序列化与反序列化方式到此结束了,既然java已经提供了序列化方法,那安卓为啥也搞个自己的序列化方法呢,其实安卓的序列化主要是在内存中进行序列化,效率更高。下面我们先开一下安卓的序列化方式。
8 安卓的序列化方式,实现Parcelable接口
查阅谷歌官方文档,对该接口的解释是:
Interface for classes whose instances can be written to and restored from a Parcel. Classes implementing the Parcelable interface must also have a non-null static field called CREATOR of a type that implements the Parcelable.Creator interface.
意思是这个 接口是为那些可以写入Parcel以及从中恢复的类的实例来使用的,实现该接口的类必须有一个非空静态字段CREATOR,这个字段实现了Parcelable.Creator接口。
简单来说,Parcel提供了一套机制,可以将序列化的数据写到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象。
再说明白点:parcel的中文意思是包裹,parcelable是可打包的意思,就是说,如果一个对象是可打包的(实现Parcelable接口),那么它就可以被打包进包裹里(parcel),而包裹可以在内存中传输,每个进程都认识,并且可以从包裹里拿出被打包的对象。
比如,如下:
User2 user = new User2("Tom","123456",88);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
Parcel parcel = Parcel.obtain();
parcel.writeParcelable(user,0);
User2 newUser = parcel.readParcelable(User2.class.getClassLoader());
Log.e("newuser",user.toString());
}
下面我们试一下这个方法,新建一个User2类。并实现Parcelable接口:
public class User2 implements Parcelable{
private String name;
private String password;
private int age;
public User2(String name, String password, int age) {
this.name = name;
this.password = password;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeString(password);
dest.writeInt(age);
}
public static final Creator<User2> CREATOR = new Creator<User2>() {
@Override
public User2 createFromParcel(Parcel source) {
User2 user = new User2("","",0);
user.name = source.readString();
user.password = source.readString();
user.age = source.readInt();
return user;
}
@Override
public User2[] newArray(int size) {
return new User2[size];
}
};
@Override
public String toString() {
return "{ name: "+name+" passwd: "+password+" age: "+age+" }";
}
}
首先在writeToParcel方法里写上我们要序列化的对象,然后在createFromParcel方法里进行反序列化操作。这个是固定的套路。用起来就比较简单了:
在Activity中的页面跳转时一般这样用:
User2 user = new User2(“Tom”,”123456”,88);
Intent intent = new Intent(this,OtherActivity.class);
intent.putExtra(“user”,user);
startActivity(intent);
在另一个页面一般直接获取就行
User2 user = getIntent().getParcelableExtra(“user”);
总结:
- android在内存序列化上,应尽量使用parcelable方式,效率更高,将一个完整的对象进行分解,或者说他只是把对象的字段进行序列化了,反序列化的时候,先手动创建一个对象,然后给对象的字段分别进行赋值,这样效率更高。android的跨进程通信传输的对象就是实现parcelableh接口;Serialiable将整个对象序列化,使用了反射机制,效率略低,这种机制会在序列化的时候创建许多的临时对象,容易触发垃圾回收
- Serialiable方式实现起来较为简单,parcelable方式实现较为复杂,
- 序列化到存储设备上时,还使用Serialiable方式