Serializable & Parcelable

对象序列化的简单介绍

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

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

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

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

使用Serializable 序列化对象

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

例如,咱们想让一个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);

复制代码

上面代码就完成了写入对象的操做,要想读回对象的话须要用到ObjectInputStreamui

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

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

// 读取mike
Person p2 = (Person) in.readObject(); 
复制代码

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

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

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

使用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 的使用并无那么简单,它牵扯出了一大堆进程间通讯相关的问题,待学习到进程间通讯时须要再从新梳理一遍。

相关文章
相关标签/搜索