数据序列化:Serializable、Externalizable 与 Parcelable 的使用探索

595 阅读7分钟

1. 序列化与反序列化的概念

  1. 序列化:将数据结构或对象转换成可存储或可传输的形式的过程,这个形式通常为二进制串
  2. 反序列化:将在序列化过程中生成的二进制串转换成数据结构或对象的过程。

持久化:是指数据在非暂存介质(如硬盘、数据库、或任何非易失性存储)上被保存的过程,以便即便在程序运行结束或设备重启之后,数据仍能够保留下来。

持久化通常需要应用序列化。

Android 中序列化的主要应用场景

  1. Intent 数据传递
  2. 数据持久化
  3. 进程间通信
  4. 网络通信
  5. 远程方法调用
  6. ...

Android 中序列化的主要方案:

  1. Serializable:Android 支持 Java 标准的序列化机制,通过实现 java.io.Serializable 接口。
  2. Parcelable:Android 独有的序列化机制。
  3. JSON、XML、Protobuf 等序列化。

合理选择序列化方案需要考虑的因素:

  • 通用性
  • 健壮性
  • 可读性
  • 可扩展性
  • 性能
  • 安全性

2. Serializable

Serializable 是 Java 提供的一个序列化接口,通过实现 Serializable 接口进而实现序列化:

package java.io;
public interface Serializable {  
}

Serializable 接口中没有任何方法,是一个空接口,如同一个标识一样。

在 Java 中实现 Serializable 接口后,JVM 会在底层帮助我们实现序列化和反序列化。

新建 Student 类并实现 Serializable 接口:

public class Student implements Serializable { 
    public static final long serialVersionUID = 1L;
    
    private String name;  
    private int id;  

    public Student(String name, int id) {  
        this.name = name;  
        this.id = id;  
    }  

    public String getName() {  
        return name;  
    }  

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

    public int getId() {  
        return id;  
    }  

    public void setId(int id) {  
        this.id = id;  
    }  
}

接着使用 ObjectOutputStream 和 ObjectInputStream 就可以实现对对象的序列化和反序列化。

创建 Student 对象,并将其进行序列化:

Student student = new Student("zhangsan", 1020230);  
saveObject(student, String.valueOf(externalStorageDirectory));

public boolean saveObject(Object object, String path) {  
    if (object == null){  
        return false;  
    }  
    ObjectOutputStream oos = null;  
    try {  
        oos = new ObjectOutputStream(new FileOutputStream(path + "/a.out"));  
        oos.writeObject(object);  
        oos.close();  
        return true;  
    } catch (IOException e) {  
        e.printStackTrace();  
    } finally {  
        if (oos != null){  
    try {  
        oos.close();  
    } catch (IOException e) {  
        e.printStackTrace();  
            }  
        }  
    }  
    return true;  
}

到对应目录下会发现 a.out 文件,通过序列化将 Student 对象持久化到本地,打开该文件:

image.png

我们还可通过反序列化将序列化到本地的文件读取出来:

Student student = readObject(String.valueOf(externalStorageDirectory));  
Log.d(TAG, "student name: " + student.getName()); // student name: zhangsan
Log.d(TAG, "student id: " + student.getId()); // student id: 1020230


public Student readObject(String path) {  
    ObjectInputStream ois = null;  
    try {  
        ois = new ObjectInputStream(new FileInputStream(path + "/a.out"));  
        return (Student) ois.readObject();  
    } catch (Exception e) {  
        e.printStackTrace();  
    } finally {  
        if (ois != null){  
            try {  
                ois.close();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
    return null;  
}

恢复后的 Student 对象与之前的对象内容完全一样,但并不是同一个对象。

在定义 Student 类时,指定了一个 serialVersionUID 标识,此标识主要作为版本控制使用的。

若没有指定 serialVersionUID,JVM 在序列化时会自动生成一个 serialVersionUID,然后与属性一起序列化。如果在反序列化时,当前类有所改变,JVM 会重新计算生成一个新的 serialVersionUID,此 serialVersionUID 与之前的 serialVersionUID 不一致,就会导致反序列化失败。

如果显示指定了 serialVersionUID,序列化时和反序列化时的 serialVersionUID 是相同的,为我们指定的值。所以指定 serialVersionUID 的值,可以避免反序列化失败。

静态变量属于类不属于对象,序列化是针对对象而言的,所以静态变量不会参与序列化的过程。

那可能有疑问:为什么 serialVersionUID 可以序列化?

并不是 serialVersionUID 属性进行了序列化,JVM 在序列化对象时会自动生成一个 serialVersionUID,然后将指定的 serialVersionUID 属性值赋给自动生成的 serialVersionUID。

若部分字段不想进行序列化,只需要使用 transient 关键字声明该字段,序列化运行时会跳过该字段的处理。

private transient int id;

3. Externalizable

Externalizable 同样是 Java 提供的一个序列化接口,Externalizable 接口继承了 Serializable。相比于 Serializable,Externalizable 在序列化与反序列化过程中,提供了更为精细的控制

Externalizable 接口:

public interface Externalizable extends java.io.Serializable {  

    void writeExternal(ObjectOutput out) throws IOException;  
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;  
}

实现 Externalizable 接口,需要实现两个方法:

  1. writeExternal(ObjectOutput out):此方法在序列化时被调用,用于将对象的状态写入到一个输出流中。开发者可以自定义序列化逻辑,选择性地仅序列化对象中的某些属性
  2. readExternal(ObjectInput in):此方法在反序列化时被调用,用于从一个输入流中读取并恢复对象的状态。定义了如何从流中读取数据,并根据读取的数据恢复对象的状态。

通过实现这两个方法,开发者可以对某个类的序列化过程进行完全的控制,这对于一些特殊场景非常有用,例如当你需要序列化一个复杂对象,但又不想序列化它的所有属性,或者需要以某种特殊方式处理某些字段时。

接下来举例说明如何使用 Externalizable 接口进行序列化和反序列化。

新建 Course 类,只对 name 属性进行序列化:

public class Course implements Externalizable {  
    private String name;  
    private int id;  

    // Externalizable 需要实现一个无参数的构造器  
    public Course() {  
    }  

    public Course(String name, int id) {  
        this.name = name;  
        this.id = id;  
    }  

    // getter() and setter()

    // 实现该方法序列化对象  
    @Override  
    public void writeExternal(ObjectOutput out) throws IOException {  
        out.writeObject(name);  
    }  

    // 实现该方法反序列化对象  
    @Override  
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {  
        name = (String) in.readObject();  
    }  

    @NonNull  
    @Override  
    public String toString() {  
        return "User{name='" + name + '\'' + ", id=" + id + '}';  
    }  
}

实现 Externalizable 接口的类必须提供一个公共的无参构造函数用于在反序列化时创建实例,JVM 会在反序列化过程中通过无参构造函数来实例化对象然后才能通过 readExternal 方法填充该对象的状态。如果没有无参构造器,JVM 就无法创建对象实例,进而无法完成反序列化过程。

接着使用 ObjectOutputStream 和 ObjectInputStream 就可以实现对对象的序列化和反序列化,与上述 Seriaizable 过程完全一致,此处代码省略。

如果新建对象 course,并将其进行序列化:

Course course = new Course("Chinese", 1);

反序列化时获得的对象将会是:

course: Course{name='Chinese', id=0}

为什么反序列化出的对象的 id 属性为 0 呢?

序列化后该对象会被写入外部存储,但只包含 name 属性。当从外部存储反序列化该对象时,由于只会恢复 name 属性,id 属性将保持为其默认初始值0。

4. Parcelable

Parcelable 是 Android 中的一个接口,实现 Parcelable 接口就可以实现序列化,新建 Teacher 类,并实现 Parcelable 接口:

public class Teacher implements Parcelable {  
    private String name;  
    private int age;  

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

    protected Teacher(Parcel in) {  
        name = in.readString();  
        age = in.readInt();  
    }  

    public static final Creator<Teacher> CREATOR = new Creator<Teacher>() {  
        @Override  
        public Teacher createFromParcel(Parcel in) {  
                return new Teacher(in);  
        }  

        @Override  
        public Teacher[] newArray(int size) {  
            return new Teacher[size];  
        }  
    };  

    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  
    public void writeToParcel(@NonNull Parcel dest, int flags) {  
        dest.writeString(name);  
        dest.writeInt(age);  
    }  
}

Parcelable 中方法:

  1. describeContents():返回当前对象的内容描述,一般情况下返回 0,仅当当前对象中存在文件描述符时,此方法返回1.
  2. writeToParcel(@NonNull Parcel dest, int flags):完成序列化功能。
  3. createFromParcel(Parcel in):完成反序列化功能。
  4. newArray(int size):创建指定长度的原始对象数组。
  5. Teacher(Parcel in):从序列化后的对象中创建原始对象。

Parcel 内部包装了可序列化数据,可以在 Binder 内自由传输。

Serializable 是 Java 中的序列化接口,使用简单,但是开销大,需要大量的 I/O 操作。

Parcelable 是 Android 的序列化方式,虽然实现起来稍微麻烦点,但是效率高,Android 推荐的序列化方式,主要为内存序列化。

Android系统提供了许多实现了 Parcelable 接口的类,他们都可以直接进行序列化,如 Intent、Bundle、Bitmap等。