Serializable & Parcelable

对象序列化的简单介绍

所谓对象的序列化其实就是把JVM运行过程中生成的对象通过特殊的处理手段转换为字节形式的文件。转换之后就可以将其永久保存到磁盘中,或者以字节流进行网络传输。

在Android中使用Intent传递数据时,基本数据类型可以直接传递,而比较复杂的引用类型的数据就需要先将对象序列化再进行传递。

序列化的转换只是将对象的属性进行序列化,不针对方法进行序列化。

Android中有两种实现序列化的方法,一种是实现Java提供的Serializable接口,另一种是Android提供的Parcelable接口。

使用Serializable 序列化对象

Serializable是Java提供的接口(interface),它里面没有任何的属性跟方法,纯粹就是起到个标识的作用。如果想让某个类下的对象能够序列化需要先实现Serializable接口。

例如,我们想让一个Person类的对象能够序列化,这个类就需要被声明为:

public class Person implements Serializable{

    private String name;
    private int age;
    
    ...
}

之后我们就可以将Person类的对象序列化写入文件中永久保存了,这个环节你需要ObjectOutStream的帮助:

// 构造一个指定具体文件的ObjectOutStream ,path为文件的路径
ObjectOutputStream out = new ObjectOutputStream(Files.newOutPutStream(path));

//实例化对象
Person peter = new Person("peter" , 18);
Person mike = new Person("mike" , 20);

// 写入对象
out.writeObject(peter);
out.writeObject(mike);

上面代码就完成了写入对象的操作,要想读回对象的话需要用到ObjectInputStream

ObjectInputStream in = new ObjectInputStream(Files.newInPutStream(path));

// 读取 peter
Person p1 = (Person) in.readObject(); 

// 读取mike
Person p2 = (Person) in.readObject(); 

注意!读取对象的顺序与写入对象的顺序是一致的。

如果序列化对象的属性是基本数据类型的则会以二进制形式保存数据,如果属性也是一个对象那么它会被writeObject()再次写入,直到所有属性都是基本数据类型为止。

还有一点,如果写入的两个对象里引用了同一个对象,当读取回这两个对象时它们引用的对象还是同一个,而不会是两个内容相同却是不同引用的对象。这归功于在读写对象时会为每个对象记录一个唯一序列号。

使用transient关键字忽略某些属性

在实际中某些属性是不需要被序列化的,例如数据库连接对象就没必要序列化,为了实现某些属性不被序列化,我们可以给这些属性加上一个transient修饰标记符,那么这些属性在序列化时就会被自动忽略。

public class Person implements Serializable{

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;
    ...
}

关于序列化版本

有时候我们会将序列化的对象从一台JVM传到另一台JVM上运行,为保证读取的对象与写入的对象一致,JVM在写入对象的时候为类分配了一个serialVersionUID属性.

serialVersionUID属性用来标识当前序列化对象的类版本,如果我们没有手动指定它,JVM会根据类的信息自动生成一个UID。但如果是两台JVM互传数据时为保证类的一致性,我们最好自己手动声明这个属性:

public class Person implements Serializable{

    // 序列化的版本,自己定义具体数据来实现每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;
    ...
}

自定义序列化细节

到现在为止我们序列化对象的方法只是直接调用了Java的API,序列化的过程全部由Java帮我们默认实现。但是有些情况我们需要在序列化时进行一些特殊处理,例如某些表示状态的属性序列化时不需要保存而反序列化成对象时希望能够被赋值,显然transient关键字不能帮我们实现,这时候我们就需要自定义序列化的细节。

ObjectOutputStreamObjectInputStream在序列化与反序列化时会检查我们的类是否声明了如下几个方法:

  • void writeObject(ObjectOutputStream oos) throws IOException
    序列化对象时调用的方法
  • void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException
    反序列化对象时调用的方法
  • Object writeReplace() throws ObjectStreamException ObjectOutPutStream
    序列化对象之前调用的方法,在这里可以替换真正被序列化的对象
  • Object readResolve() throws ObjectStreamException
    在反序列化对象后调用的方法,在这里可以替换反序列化后得到的对象

以上方法如果你自己声明了那么就执行你自定义的方法,否则使用系统默认的方法。至于自定义方法的权限修饰符private protected public都无所谓,因为使用ObjectXXXputStream使用反射调用的。他们在序列化与反序列化的调用流程如下图。

序列化与反序列化流程

此四个方法你可以根据需要任意替换成自己的方法,不过一般都是都是读写成对替换的,下面看我们如何用自定义方法实现序列化:

public class Person implements Serializable{

    // 序列化的版本,自己定义具体数据来实现每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;

    public Person(String name,int age){
        this.name = name;
        this.age = age;  
    } 

    private void writeObject(ObjectOutputStream oos)  throws IOException {
        // 默认的序列化对象方法
        out.defaultWriteObject();
        //我们自定义添加的东西
        out.writeInt(100);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
          // 默认反序列化方法
         in.defaultReadObject();
         // 读出我们自定义添加的东西
         int flag = in.readInt();
         System.out.println(flag);
    }

    private Object writeReplace(){
         // 替换真正序列化的对象
         return new Person(name,age);
    }

    private Object readResolve(){
        // 替换反序列化后的对象
        return new Person(name,age);
    }
}

此处你可能对writeReplace()readResolve()方法的用处有疑问,在下面序列化代理中会见识到它们的用处。

关于反序列化需要注意的

从字节流中读取的数据后反序列化的对象并不是通过构造器创建的,那么很多依赖于构造器保证的约束条件在对象反序列化时都无法保证。比如一个设计成单例的类如果能够被序列化就可以分分钟克隆出多个实例...

序列化代理

在知道了Java在反序列化时并不是通过构造器创建的对象,那么别人只需要解析你序列化后的字节码就能够轻而易举的获取你的内容,不仅如此,再利用同样的序列化格式生成任意的字节码送你你的程序分分钟就攻破你的程序。

为解决该隐患,大神们推荐我们使用静态内部类作为代理来进行类的序列化:

public class Person implements Serializable{

    // 序列化的版本,自己定义具体数据来实现每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不需要序列化的属性
    private transient Connection mConn;

    public Person(String name,int age){
        this.name = name;
        this.age = age;  
    } 

    // 把真正要序列化的对象替换成PersonProxy 代理
    private Object writeReplace() {
        return new PersonProxy (this);
    }

    // 因为真正被序列化的对象是PersonProxy 代理对象,所以Person的readObject()方法永远不会执行
    // 执行的是PersonProxy 代理对象的readObject()方法
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        // 如果该方法被执行说明有流氓入侵,直接抛异常
        throw new InvalidObjectException("proxy requied");
    }

    static class PersonProxy implements Serializable{
        private String name;
        private int age;

        public PersonProxy(Person person){
            this.name = person.name;
            this.age = person.age;
        }

        // 把读取出来的代理对象再替换回Person对象
        private Object readResolve(){
            return new Person(name,age);
        }
    }
}

使用Parcelable 序列化对象

Parcelable 虽然也是序列化对象的方法,但是它跟java提供的Serializable 在使用上有着极大的差别。

Android设计 Parcelable 的目的是让其支持进程间通信的功能,因此它不具备类似Serializable的版本功能,所以Parcelable 不适合永久存储。

实现Parcelable 接口需要满足两个条件:

  1. 实现Parcelable 接口下的两个方法describeContents()writeToParcel(Parcel out,int flags)

  2. 声明一个非空的静态属性CREATOR且类型为Parcelable.Creator <T>

例如我们想让person类实现Parcelable 接口:

public class Person implements Parcelable {

     private int age;

     // 定义当前传送的 Parcelable实例包含的特殊对象的类别
     public int describeContents() {
         // 一般情况我们用不到,直接为0就行 
         return 0;
     }

     // 在该方法中将对象的属性写入字节流
     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(age);
     }

     // 该静态属性会从Parcel 字节流中生成Parcelable类的实例
     public static final Parcelable.Creator<MyParcelable> CREATOR
             = new Parcelable.Creator<Person>() {
         
         // 该方法接收Parcel解析成对应的实例
         public Person createFromParcel(Parcel in) {
             return new Person(in);
         }
        
         // 根据size创建对应数量的实例数组
         public Person[] newArray(int size) {
             return new Person[size];
         }
     };
     
     private Person(Parcel in) {
         age= in.readInt();
     }
 }

到此Person就具备了序列化的条件。至于读和写就看具体的需求了,最简单的使用方法可以利用Intent传递。

让我惊讶的是Parcelable 的使用并没有那么简单,它牵扯出了一大堆进程间通信相关的问题,待学习到进程间通信时需要再重新梳理一遍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容