序列化Serializable和Parcelable

概念

Java中的序列化是一种将对象持久化(比如存储在磁盘)的手段。一般情况下,程序运行(即JVM运行)时,Java对象(短暂)存储在内存中。但JVM停止运行后,对象的状态信息就不能保存在内存了。我们需要将对象持久化保存,这就是序列化的用途。
简单说,序列化就是,将对象的状态信息转换为字节序列的过程。通过序列化,可以将对象的状态信息转换为字节序列,然后通过IO流保存到磁盘或者网络传输。然后从磁盘或网络请求获取到字节流,通过反序列化,重新创建该对象。

Serializable

Serializable是Java提供的序列化接口,实现Serializable接口的类,极为可序列化的类。可以直接通过ObjectOutputStream序列化,也可以通过ObjectInputStream反序列化。

以下便是Java序列化的典型用法

public class SerializableTest implements Serializable {

    private final static long serialVersionUID = 1L;
    private String name;
    private String job;
    private UnSerializable unSerializable;

    public SerializableTest(String name, String job,UnSerializable unSerializable) {
        this.name = name;
        this.job = job;
        this.unSerializable = unSerializable;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public UnSerializable getUnSerializable() {
        return unSerializable;
    }

    public void setUnSerializable(UnSerializable unSerializable) {
        this.unSerializable = unSerializable;
    }
}
serialVersionUID

serialVersionUID是类的序列化ID,可以理解为这个类的版本号。serialVersionUID可以自己手动声明,也可以由系统默认。系统默认的serialVersionUID是根据类的定义计算出一个serialVersionUID。如果类的定义发生了改变,默认的serialVersionUID也会随之变化,就像类的版本发生了改变一样。
serialVersionUID的作用在于,虚拟机是否允许反序列化,不仅仅是取决于类的路径和功能代码是否一致。还有一个非常重要的判断依据是,两个类的serialVersionUID是否一致。

具体的场景就是:客户端A和B通过网络传递对象数据,客户端A将c对象序列化为字节流,通过网络传输给客户端B,然后B反序列化得到c。此时如果A和B端的app版本不同,而导致C类的serialVersionUID不同,就会导致B端反序列化失败。
当然,有时候,我们也可以通过这种方法,强制app的低版本升级

Java官方是建议我们手动声明serialVersionUID。一般情况下没有特殊需求,serialVersionUID就直接声明为1L。

成员变量序列化

一个对象的成员变量是不可序列化的对象的引用,会导致对象序列化失败,抛出NotSerializableException异常。序列化要求,对象的成员变量也是可序列化的。

静态变量序列化

序列化保存的是对象的状态信息,静态变量属于类的状态,因此,序列化不保存静态变量。

父类的序列化

一个子类实现了Serializable接口,它的父类没有实现Serializable接口。序列化该子类对象,然后反序列化输出其父类定义的变量的值,该变量的值和序列化之前不同。
如果父类没有实现Serializable接口,虚拟机是不会序列化父类对象的。此时就要求,父类必须有无参构造函数。而一个Java对象的构造必须先有父类对象,再创建子类对象,反序列化时也不例外。所以反序列化时,虚拟机只能调用父类的无参构造函数来创建父类对象。这样,父类定义的成员变量的值,就是调用无参构造函数后的值。如果你考虑到了这种序列化的情况,就可以在父类的无参构造函数对成员变量赋值。

Transient

Transient关键字的作用就是控制变量的序列化,在变量声明前声明Transient,表示该变量不会被序列化。在反序列化时,该变量值就会是该类型的初始值,如int型为0,String型为null。

自定义readObject和WriteObject

将对象序列化后,进行网络传输,就要考虑到数据的安全性问题。这样的场景,可以考虑封装自定义的writeObject和readObject方法。这样可以序列化时,对数据进行加密,反序列化时解密。

public class SerializableTest implements Serializable {
    private final static long serialVersionUID = 1L;
    private String name;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    private void writeObject(ObjectOutputStream out) {
        try {
            ObjectOutputStream.PutField putFields = out.putFields();
            System.out.println("原name:" + name);
            name = "encryption-"+name;//模拟加密
            putFields.put("name", name);
            System.out.println("加密后的name" + name);
            out.writeFields();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

      private void readObject(ObjectInputStream in) {
        try {
            ObjectInputStream.GetField readFields = in.readFields();
            Object object = readFields.get("name", "");
            System.out.println("要解密的字符串:" + object.toString());
            name = object.toString().split("-")[1];//模拟解密
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
序列化存储规则

Java序列化机制为了节省磁盘空间,具有特定的存储规则,当多次序列化写入文件的是同一对象时,并不会再将对象的内容写入文件,而只是再存储一个引用,此时写入文件的就是新增的引用和一些控制信息。反序列化时,恢复引用关系,使得所有的引用指向的是同一个对象。这样的特点,会带来另一个值得注意的问题:

File file = new File("H:/sourceCode/workspace4java/test.txt");
            SerializableTest testObject = new SerializableTest("Smith","engineer",32,new UnSerializable("1024"));
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(testObject);
            oos.flush();
            testObject.setName("James");
            oos.writeObject(testObject);
            oos.close();
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            SerializableTest result1 = (SerializableTest)ois.readObject();
            SerializableTest result2 = (SerializableTest)ois.readObject();
            System.out.println(result1.getName());
            System.out.println(result2.getName());

输出结果是

Smith
Smith

我们希望看到的是第二次反序列化的对象name为James,但是可以看到,两次反序列化的对象的name一样。这是因为第二次序列化写入时,虚拟机根据引用关系,已经判断出写入的是同一对象,因此只保存了第二次的引用。所以反序列化读取时,都是第一次保存的对象。

Parcelable

Parcelable是Android提供的序列化接口。它相比于Serializable的优点是,序列化的效率要高一些。 实现了 Parcelable 接口的类在序列化和反序列化时会被转换为 Parcel 类型的数据 。 Parcel 是一个载体,它可以包含数据或者对象引用,然后通过 IBinder 在进程间传递。
Parcelable和Serializable的对比:

  • 一般在保存数据到 SD 卡或者网络传输时建议使用 Serializable 即可,虽然效率差一些,好在使用方便。
  • 而在运行时数据传递时建议使用 Parcelable,比如 Intent,Bundle 等,Android 底层做了优化处理,效率很高。

本文参考:
[https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html
https://www.hollischuang.com/archives/1140
https://blog.csdn.net/u011240877/article/details/72455715

    原文作者:zackyG
    原文地址: https://www.jianshu.com/p/6b4947644f81
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞