Serializable、Parcelable序列化与反序列化

前言

在学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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值