前言
在学Java的IO流时学到序列化流与反序列化流,想到之前学长有问过我知不知道JSON什么序列化反序列化啥的(可以将对象序列化为JSON格式的数据,有利于网络传输与存储),当时一脸懵圈。决定写一篇文章来彻底搞懂一下这个序列化反序列化。
序列化与反序列化
首先我们需要了解序列化与反序列化到底是个啥玩意儿。序列化与反序列化是编程中的基本操作,与数据存储、传输、和处理密切相关。 序列化负责将数据结构转化为可存储和可传输的格式(字节序列),而反序列化则是这个过程的逆操作。
简单来讲就是:
- 序列化:把对象转为字节序列的过程
- 反序列化:把字节序列转为原来对象的过程
那这个序列化反序列化的目的是啥呢?
- 首先,序列化之后对象变为了字节序列,这样就有利于持久化存储在磁盘上,这样我们是不是就相当于有了一个“可持久化对象”呢
- 其次,转为了字节序列也有利于网络传输
下面要介绍的Serializable(Java提供的)和Parcelable(Android提供的)就是实现序列化与反序列化的两种方式。
Serializable方式
对象序列化成字节序列
现在的Java是还没有一个关键字能去直接定义一个所谓的“可持久化对象”(存储了对象信息的字节序列)
这个序列化反序列化的过程是需要我们通过Java中的序列化流、反序列化流来完成的。
那在这我们先来简单介绍一下序列化流与反序列化流:
- 序列化流(ObjectOutputStream) 又称对象操作输出流。可以把Java中的对象以字节序列的形式写到本地文件。
- 反序列化流(ObjectInputStream) 又称对象操作输入流。可以把序列化到本地文件中的对象读取出来。
那么在了解到序列化流与反序列化流之后我们就可以来举个例子了,假如我们现在要把一个Student类对象序列化到本地student.txt文件中,然后通过反序列化重新得到这个Student对象:
- 创建Student类
public class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 通过序列化流将Student对象写到本地文件中
public class ObjectStreamDemo1 {
public static void main(String[] args) throws IOException {
Student student=new Student("蔡徐困",25);
//创建序列化流对象
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("student.txt"));
//写出数据
objectOutputStream.writeObject(student);
//释放资源
objectOutputStream.close();
}
}
- 运行ObjectStreamDemo1
哎呦我去,咋爆红啦,心态崩了。
别急,我们来看看报错。提示我们出了个NotSerializableException异常。
其实解决方法很简单,我们只需要去实现一个Serializable接口。
我们按住ctrl点击Serializable进去看看
这里面啥也没有啊。我们把这种里面没有任何抽象方法的接口,称为标记型接口。一旦实现这个接口之后,就表示我们当前的这个类可以被序列化了。
OK重新运行ObjectStreamDemo1,完美没有任何报错,此时我们打开student.txt文件就发现这个对象已经写到了本地文件中。
字节序列反序列化为对象
- 通过反序列化流将文件中的对象读出来
public class ObjectStreamDemo2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//创建反序列化流对象
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("student.txt"));
//读取数据
Student student=(Student) objectInputStream.readObject();
//打印对象
System.out.println(student.toString());
//释放资源
objectInputStream.close();
}
}
- 运行ObjectStreamDemo2
可以看到非常完美地打印出了我们的蔡同学。
SerialVersionUID
但此时我们这仍是存在一点问题的。我们可以试着先运行ObjectStreamDemo1再稍微去修改一下Student类(例如加一个gender属性),再去运行一下ObjectStreamDemo2。我们会发现
不出意料爆红了,报错提示两次serialVersionUID
不一样。
这就需要我们了解到,在Java底层,会根据Student这个类里面所有的信息计算出一个序列号(版本号),我们再去ObjectStreamDemo1创建对象时其实也就包括了这个序列号,这个序列号会随着序列化也保存到文件中的字节序列里去,然而我们再对Student类进行修改,此时Java底层会给Student类重新计算出一个序列号。接着我们运行ObjectStreamDemo2,两个序列号不一样,代码直接报错。
所以我们知道了,问题的原因就在于文件中的序列号与Student类中的序列号不一样。
如何解决
要想解决上面的问题,我们直接让serialVersionUID保持不变就行了吗。我们可以直接在Student中定义好serialVersionUID。
推荐让Java帮我们自动生成。
要想自动生成,先去setting设置一点东西。
这样我们在每次写好一个类的时候只需alt+enter类名就可以自动根据我们这个类里的信息计算出一个serialVersionUID了。
这样我们此时的Student类就是这样的了
public class Student implements Serializable {
//序列号
private static final long serialVersionUID = -5890135746201264030L;
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
getter、setter省略了...
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
此时重新进行上述操作,之前的问题就不复存在了。
transient
瞬态关键字,作用为不会把当前关键字序列化到本地文件中(常用于一些比较隐私的属性,如密码、家庭住址等)。
例如此时我们在Student类中加一个家庭住址属性
private transient String address;
在去创建对象时传入家庭住址为北京
Student student=new Student("蔡徐困",25,"北京");
然后反序列化,我们会发现控制台打印的address=null。这就是transient的作用,不参与序列化,值默认为null。
在Android中使用
在Android使用Intent来启动活动、发送广播、启动服务时,我们可以使用putExtra()方法来传递一些数据,但putExtra()能传递是数据类型是有限的,如果我们想传递自己定义的对象时,只能将对象序列化为字节序列来进行传递,再通过反序列化来得到对象。
接下来我们通过一个实例来进行演示如何使用(在FirstActivity中启动SecondActivity并传递一个Person对象给SecondActivity):
- 让Person类实现Serializable接口
public class Person implements Serializable{
private String name;
private int age;
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age=age;
}
}
- 在FirstActivity启动SecondActivity并传递一个Person对象
Person person=new Person("iKun",20);
Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("person_date",person);//只有实现了Serializable接口才可以这样写
startActivity(intent);
- 在SecondActivity中反序列化得到Person对象
Person person=(Person) getIntent().getSerializableExtra("person_date");
Parcelable方式
除Serializable之外,Parcelable也可以实现相同的效果,但不同的是Parcelable将对象进行序列化的原理是将一个完整的对象进行分解,而分解后的每一部分数据都是Intent支持的数据类型。接下来依旧通过一个案例演示:
- 将上述的Person类进行修改(实现Parcelable接口)
public class Person implements Parcelable{
private String name;
private int age;
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age=age;
}
@Override
public int describeContents(){
return 0;
}
@Override
piblic void writeToParcel(Parcel dest,int flags){//将Person类中的数据一一写出
dest.writeString(name);
dest.writeInt(age);
}
public static final Parcelable.Creator<Person> CREATOR=new Parcelable.Creator<Person>(){
@Override
public Person createFromParcel(Parcel source){//读取writeParcel()方法写出的数据并将Person对象返回
Person person=new Person();
person.name=source.readString();
person.age=source.readInt();
return person;
}
@Override
public Person[] newArray(int size){
return new Person[size];
}
};
}
- 在FirstActivity启动SecondActivity并传递一个Person对象
Person person=new Person("iKun",20);
Intent intent=new Intent(FirstActivity.this,SecondActivity.class);
intent.putExtra("person_date",person);
startActivity(intent);
- 在SecondActivity中反序列化得到Person对象
Person person=(Person) getIntent().getParcelableExtra("person_date");
在Android中,Serializable的实现方法简单点,但需要将整个对象进行序列化,所以效率相比Parcelable低点,所以在Android中推荐使用Parcelable方式来实现Intent传递对象的功能。
Parcel
上面Person类是通过Parcel写入和读取恢复数据的,并且必须要有一个非空的静态变量CREATOR。Parcel是一个用于打包和传递数据的容器,它可以被看作是一个轻量级的序列化和反序列化工具。
简单来说,Parcel有一套机制,可以将序列化后的数据写入一个共享内存,其它进程可以通过Parcel从这块共享内存中读取出字节流并反序列化为对象,从而实现数据传递的功能。这种机制可以用于Intent数据传递,但主要还是用于Binder机制,也就是Android进程间通信,这里不对这方面扩展。
Serializable和Parcelable的区别
首先这两个都可以在Android中实现使用Intent传递对象的功能。
-
但不同的是Serializable方式实现过程中需要频繁涉及IO操作且将整个对象进行了序列化,所以Serializable方式的效率较低。
-
Serializable只需实现Serializable接口即可使用,Parcelable还需要重写各种方法,所以Serializable的实现相比Parcelable简单。
-
如果需要存储到设备或者在网络上传输,使用Serializable(它将整个对象都进行了序列化,适用于磁盘上的存储与网络传输)。如果在内存中进行数据传输时,使用Parcelable。