序列化与反序列化之Serializable和Parcelable

面试场景

Android 开发中对两个 Activity 之前传递数据,应该很熟悉吧?

嗯,当然没问题。一般采用 Intent.putXXX() 就可以实现各种轻量级数据的传递。

那对于自定义的 Object 呢?

直接使用 Bundle.putSerializable() 即可。需要把对象实现 Serializable 接口,最后使用 Intent.putExtras(Bundle) 把数据放进 Intent 即可。

除了这种方式,还有其它方式吗?和这种方式有什么区别呢?

我知道还有 Bundle.putParcelable() ,不过我们平时基本都只用 Serializable 方式。

为什么不用 Parcelable 方式呢?它们有什么不同呢?

因为简单呀,Serializable 方式只需要实现接口一句代码就好了,Parcelable 我记得有很多代码。对于它们的区别嘛,em……额……嗯…….

开发Android也有些时间了,Serializable和Parcelable遇到过不止一次了。但是每次面试问起具体的内容时知道的不是很清晰。所以学习并梳理一下,以文章的形式给自己存储下来。温故而知新~!~!

序列化和反序列化

序列化:将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区(磁盘或者其他介质)。

反序列化:就是读取序列化后保存在存储区的序列化信息或反序列化对象的状态,重新创建该对象。

一个对象要实现序列化操作,该类就必须实现了Serializable接口或者Parcelable接口,其中Serializable接口是在java中的序列化抽象类,而Parcelable接口则是android中特有的序列化接口,在某些情况下,Parcelable接口实现的序列化更为高效。

Serializable接口

空接口Serializbale

来看看序列化 Serializbale 接口的定义:

public interface Serializable {
}

明明就一个空的接口嘛,竟然能够保证实现了它的“类的对象”被序列化和反序列化?

具体实现

我们先看一个实现Serializable接口的类,只有两个字段,和对应的 getter/setter),用于序列化和反序列化。

public class User implements Serializable {
    private static final long serialVersionUID = -4454266436543306544L;
    public int userId;
    public String userName;
    public User(int userId, String userName) {
        this.userId = userId;
        this.userName = userName;
    }

再来创建一个测试类

public class Test {

    public static void main(String[] args) {

        //序列化过程
        User user = new User(1, "Tom");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
        out.writeObject(user);
        out.close();
        //反序列化过程
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
        User newuser = (User) in.readObject();
        in.close();
    }

}

上述代码采用Serializable方式序列化对象的典型过程,只需要把实现Serializable接口的User对象写到文件中就可以快速恢复了,恢复后的对象newUser和User的内容完全一致,但是两个并不是同一个对象。

serialVersionUID的作用

如上述代码所示,User类实现的Serializable接口并声明了序列化标识serialVersionUID,实际上,甚至这个serialVersionUID也不是必需的,我们不声明这个serialVersionUID同样可以实现序列化,但是这将会对反序列化过程产生影响,具体会产生什么影响呢?

为什么需要serialVersionUID呢?

因为静态成员变量属于类不属于对象,不会参与序列化过程,使用transient关键字标记的成员变量也不参与序列化过程。 (PS:关键字transient,这里简单说明一下,Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的)

这个时候又有一个疑问serialVersionUID是静态成员变量不参与序列化过程,那么它的存在与否有什么影响呢?

具体过程是这样的:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。

声明serialVersionUID对进行序列化有啥影响?

如果不手动指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的hash值并且把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化的数据中的serialVersionUID不一致,于是反序列化失败。所以我们手动指定serialVersionUID的值能很大程度上避免了反序列化失败。

如果User没有实现Serializbale 接口,所以在运行测试类的时候会抛出异常,堆栈信息如下:

java.io.NotSerializableException: com.cmower.java_demo.khb.User
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at com.cmower.java_demo.khb.Test.main(Test.java:21)

顺着堆栈信息,我们来看一下 ObjectOutputStream 的 writeObject0() 方法。其部分源码如下:

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());
    }
}

也就是说,ObjectOutputStream 在序列化的时候,会判断被序列化的对象是哪一种类型,字符串?数组?枚举?还是 Serializable,如果全都不是的话,抛出 NotSerializableException

怎么实现序列化呢?

以 ObjectOutputStream 为例吧,它在序列化的时候会依次调用 writeObject()writeObject0()writeOrdinaryObject()writeSerialData()invokeWriteObject()defaultWriteFields()

private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        desc.checkDefaultSerialize();

        int primDataSize = desc.getPrimDataSize();
        desc.getPrimFieldValues(obj, primVals);
        bout.write(primVals, 0, primDataSize, false);

        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        desc.getObjFieldValues(obj, objVals);
        for (int i = 0; i < objVals.length; i++) {

            try {
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            }
        }
    }

那怎么反序列化呢?

以 ObjectInputStream 为例,它在反序列化的时候会依次调用 readObject()readObject0()readOrdinaryObject()readSerialData()defaultReadFields()

private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        Class<?> cl = desc.forClass();
        desc.checkDefaultSerialize();

        int primDataSize = desc.getPrimDataSize();
        desc.getPrimFieldValues(obj, primVals);
        bout.write(primVals, 0, primDataSize, false);

        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        desc.getObjFieldValues(obj, objVals);
        for (int i = 0; i < objVals.length; i++) {

            try {
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            }
        }
    }

Serializable 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成。

注意点

静态成员变量属于类不属于对象,不会参与序列化过程,使用transient关键字标记的成员变量也不参与序列化过程。 关键字transient,这里简单说明一下,Java的serialization提供了一种持久化对象实例的机制。当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serialization机制来保存它。为了在一个特定对象的一个域上关闭serialization,可以在这个域前加上关键字transient。当一个对象被序列化的时候,transient型变量的值不包括在序列化的表示中,然而非transient型的变量是被包括进去的。

Parcelable接口

下面给出一个Parcelable接口的实现

public class Book implements Parcelable {
    public int bookId;
    public String bookName;

    public Book() {
    }

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }
    //从序列化后的对象中创建原始对象
    protected Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        //从序列化后的对象中创建原始对象
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }
        //指定长度的原始对象数组
        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };
    //返回当前对象的内容描述。如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0
    @Override
    public int describeContents() {
        return 0;
    }
    //将当前对象写入序列化结构中,其flags标识有两种(1|0)。
    //为1时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况下都为0.
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(bookId);
        dest.writeString(bookName);
    }

    @Override
    public String toString() {
        return "[bookId=" + bookId + ",bookName='" + bookName + "']";
    }
}

鉴于Serializable在内存序列化上开销比较大,而内存资源属于android系统中的稀有资源(android系统分配给每个应用的内存开销都是有限的),为此android中提供了Parcelable接口来实现序列化操作,Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如通过Intent在activity间传输数据,而Parcelable的缺点就使用起来比较麻烦。

从代码中可以看出,在序列化过程中需要实现的功能有序列化,反序列化和内容描述。序列化功能是由writetoParcel方法来完成,最终是通过Parcel中的一系列write方法来完成的。反序列化功能是由CREATOR方法来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化过程(PS:write和read的顺序必须一致~!);内容描述功能是有describeContents方法来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1.

系统已经给我们提供了许多实现了Parcelable接口类,他们都是可以直接序列化的,比如Intent,Bundle,Bitmap等,同事List和Map也支持序列化,提前是他们里面的每个元素都是可以序列化的。

Parcelable 与 Serializable 区别

  • 两者的实现差异
    Serializable的实现,只需要实现Serializable接口即可。这只是给对象打了一个标记(UID),系统会自动将其序列化。而Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法。
  • 两者的设计初衷
    Serializable的设计初衷是为了序列化对象到本地文件、数据库、网络流、RMI以便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。而Android的Parcelable的设计初衷是由于Serializable效率过低,消耗大,而android中数据传递主要是在内存环境中(内存属于android中的稀有资源),因此Parcelable的出现为了满足数据在内存中低开销而且高效地传递问题。
  • 两者效率选择
    Parcelable的性能比Serializable好,在内存开销方面较小,所以Android应用程序在内存间数据传输时推荐使用Parcelable,如activity间传输数据和AIDL数据传递,而Serializable将数据持久化的操作方便,因此在将对象序列化到存储设置中或将对象序列化后通过网络传输时建议选择Serializable(Parcelable也是可以,只不过实现和操作过程过于麻烦并且为了防止android版本不同而导致Parcelable可能不同的情况,因此在序列化到存储设备或者网络传输方面还是尽量选择Serializable接口)。
  • 两者需要注意的共同点
    无论是Parcelable还是Serializable,执行反序列操作后的对象都是新创建的,与原来的对象并不相同,只不过内容一样罢了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值