详解Serializable

  就个人理解而言, 序列化是对象的一种固化方式, 它实现了对象的持久化存储(当然实现方式不同,结果会有所差异).而Android存在两种序列化的方式, 一种基于IO流的序列化, 实现简单, 仅需实现Serializable接口而已, 可以不用添加额外的方法,源自Java; 另一种就是Android的原生实现方式了, 实现Parcelable接口了, 相比Serializable方式,实现起来较为复杂.下面将详细介绍两种实现方式, 以及它们被我们忽略掉的一些地方.

在GitHub工程 link 中可以找到相关代码

Serializable

先放出它的官方文档 : Interface Serializable

官方文档大致解决了以下几个问题:

  • 如何序列化一个类?

    通过实现一个Serializable接口(当然了,继承自Serializable的接口也算在内), 或者继承自一个实现了Seriaizable的类.

  • 继承自一个没有实现Serializable接口的父类的类序列化时, 应该注意哪些事项?

    在此种情况下, 父类必须要有一个显示的无参构造函数,而且其无参构造函数必须是能够对其子类可见的, 比如不能使用 private 修饰, 否则会抛出异常. 其原因在于, 在反序列化的过程中, 父类的变量会通过父类的无参构造函数进行初始化(==按照文档的意思, 似乎无参构造函数必须申明为public/protected, 但我实际试了下, 不使用也可以, 只要保证子类能正常访问就行==)

  no valid constructor
  • 如果序列化的对象引用了非静态的且不能进行序列化的对象时, 在序列化时会发生什么情况以及相关的处理措施?

    如果 不能进行序列化的对象未使用 transient 关键词进行修饰, 则会抛 NotSerializableException .

    这种情况下, 就需要对这个类进行一些特殊的处理了, 需要实现以下三个方法.

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
     
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException
     
 private void readObjectNoData()throws ObjectStreamException

下面分别介绍一下这几个方法

private void writeObject(java.io.ObjectOutputStream out) throws IOException

writeObject 方法可以写入一些无法通过默认机制序列化的对象, 并使用 readObject 方法还原回来, 其实就是一个自定义序列化的方式. 通过执行 out.defaultWriteObject 方法可以保存默认参与序列化的字段, 可以使用写入基础数据类型的方法 (例如: out.writeInt)保存特殊的字段.

private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException

readObject 方法实际上就是一个反序列化的方法, 通过 in.defaultReadObject 方法 恢复默认的参与序列化的字段, 通过 类似于 in.readInt 等方式恢复特殊的字段.

private void readObjectNoData()throws ObjectStreamException

这个方法就很少用到了, 不必去过于纠结, 如果有好的理解, 欢迎分享出来.从它的命名上就能看出来, 这是出现了序列化的流没有给出想要的数据时出现的情况.根据官方文档所述, readObjectNoData 是为了应对 当序列化的流没有给出指定的被序列化的对象的父类的时候出现的.可能翻译得不大准确, 但大概意思就是说, 对象序列化后的流在被反序列化时发现对象的父类发生变动了(这个解释是不大准确的).这种情况出现在 接收部分使用了与发送部分不同版本的反序列化实例的类, 并且接收版本的类并没有被发送版本所继承;或者是序列化流被篡改了. 因此, readObjectNoData 适用于初始化反序列化对象, 尽管流不完整或者不那么对头

为什么说上述的理解是有问题的呢?原因在于, 我无法根据以上描述来实现调用 readObjectNoData 的场景. 我在 stackoverflow 上找到了一个相关的场景, 并大致整理了一个例子, 由于比较难以搞清楚, 所以贴出源码.

例子

序列化时的代码

package th.algorithm.practice;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.HashMap;

/**
 * Created by me_touch on 18-4-12.
 *
 */

public class SerializableTest implements Serializable{

    private B b;

    public SerializableTest(){
        this.b = new B();
    }

    public void setB(int value) {
        this.b.setB(value);
        this.b.setA(value + 1);
        System.out.println(b.getB() + b.getA());
    }

    public void trySerialB(){
        try {
            File file = new File("algorithm/B.tmp");
            if(file.exists())
                file.delete();
            file.createNewFile();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            oos.writeObject(b);
            oos.flush();
            oos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void tryDeSerialB(){
        try {
            File file = new File("algorithm/B.tmp");
            if(file.exists()) {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                B b = (B)ois.readObject();
                //System.out.println(b.getB() + b.getC());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
        SerializableTest test = new SerializableTest();
        test.setB(5);
        test.trySerialB();
    }

    public static class B extends A implements Serializable{

        private int b;

        public B(){
        }

        public B(int a){
        }

        public void setB(int b) {
            this.b = b;
        }

        public int getB() {
            return b;
        }

        private void writeObject(java.io.ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
        }

        private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
            in.defaultReadObject();
        }

    }

    public static class A{

        private int a;

        /**
         * 若想子类可序列化, 则必须存在子类可访问的无参构造函数
         */
        A(){
           this.a = 0;
        }

        /**
         * 不能使用 private 修饰, 对子类不可访问
        private A(){

        }

         */
        public A(int a){
            this.a = a;
        }

        public void setA(int a) {
            this.a = a;
        }

        public int getA() {
            return a;
        }
    }
}

现在我们变更类A, 并直接进行反序列化操作, 修改过后的代码如下

package th.algorithm.practice;

import java.io.File;
import java.io.FilereadObjectNoData has been invoked
13InputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.HashMap;

/**
 * Created by me_touch on 18-4-12.
 *
 */

public class SerializableTest implements Serializable{

    private B b;

    public SerializableTest(){
        this.b = new B();
    }

    public void setB(int value) {
        this.b.setB(value);
        this.b.setA(value + 1);
        System.out.println(b.getB() + b.getA());
    }

    public void trySerialB(){
        try {
            File file = new File("algorithm/B.tmp");
            if(file.exists())
                file.delete();
            file.createNewFile();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            oos.writeObject(b);
            oos.flush();
            oos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void tryDeSerialB(){
        try {
            File file = new File("algorithm/B.tmp");
            if(file.exists()) {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                B b = (B)ois.readObject();
                System.out.println(b.getB() + b.getC());
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
        SerializableTest test = new SerializableTest();
        test.tryDeSerialB();
    }

    public static class B extends A implements Serializable{

        private int b;

        public B(){
        }

        public B(int a){
        }

        public void setB(int b) {
            this.b = b;
        }

        public int getB() {
            return b;
        }

        private void writeObject(java.io.ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
        }

        private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
            in.defaultReadObject();
        }

    }

    public static class A implements Serializable{

        private int a;
        private int c;

        /**
         * 若想子类可序列化, 则必须存在子类可访问的无参构造函数
         */
        A(){
           this.a = 0;
        }

        /**
         * 不能使用 private 修饰, 对子类不可访问
        private A(){

        }

         */
        public A(int a){
            this.a = a;
        }

        public void setA(int a) {
            this.a = a;
        }

        public int getA() {
            return a;
        }

        public void setC(int c) {
            this.c = c;
        }

        public int getC() {
            return c;
        }

        private void readObjectNoData()throws ObjectStreamException{
            setA(6);
            setC(8);
            System.out.println("readObjectNoData has been invoked");
        }
    }
}

执行结果

readObjectNoData has been invoked
13

以上就是关于这三个方法的大致介绍, 自定义序列化的过程中 readObjectNoData 不一定需要自己去实现, 而 writeObject 和 readObject 方法则是需要去实现的.

  • 如何使用特定的对象来替换序列化或者反序列化后的对象

    需要根据需求实现下列方法

 /**
  * 序列化时替换
  * 即可以使用private, protected 等权限关键词(子类的访问权限 follow java 本身的规则)
  */
 ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
 
 /**
  * 反序列化时替换
  * 可以用来解决单例被反序列化时带来的安全问题(即单例不再是单例)
  * 即可以使用private, protected 等权限关键词(子类的访问权限 follow java 本身的规则)
  */
 ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

如果在序列化的过程中使用了 writeReplace 方法, 则反序列化的过程会调用用作替代 object 的反序列化方式, 而不是原 object 的序列化方式.

父类在序列化,或者反序列化的过程中, 使用了这两个中对应的方法, 则子类也会在序列化或者反序列的过程中使用这两个中对应的方法.

例子

package th.algorithm.serial;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * Created by me_touch on 18-4-18.
 *
 */

public class SerializableReplaceTest {

    private void trySerialInstance(){
        try {
            System.out.println(SingleInstance.INSTANCE);
            File file = new File("algorithm/files/instance.tmp");
            if(file.exists())
                file.delete();
            file.createNewFile();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            oos.writeObject(SingleInstance.INSTANCE);
            oos.flush();
            oos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void tryDeSerialInstance(){
        try {
            File file = new File("algorithm/files/instance.tmp");
            if(file.exists()) {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                Object object = ois.readObject();
                System.out.println(object);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void trySerialChild(){
        Child replace = new Child();
        replace.setA(9);
        try {
            File file = new File("algorithm/files/child.tmp");
            if(file.exists())
                file.delete();
            file.createNewFile();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            oos.writeObject(replace);
            oos.flush();
            oos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void tryDeSerialChild(){
        try {
            File file = new File("algorithm/files/c.tmp");
            if(file.exists()) {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                Object object = ois.readObject();
                System.out.println(object);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void trySerial(){
        WriteReplace replace = new WriteReplace();
        replace.setA(9);
        try {
            File file = new File("algorithm/files/c.tmp");
            if(file.exists())
                file.delete();
            file.createNewFile();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            oos.writeObject(replace);
            oos.flush();
            oos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void tryDeSerial(){
        try {
            File file = new File("algorithm/files/c.tmp");
            if(file.exists()) {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                Object object = ois.readObject();
                System.out.println(object);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
        SerializableReplaceTest test = new SerializableReplaceTest();
        test.trySerial();
        test.tryDeSerial();
        test.trySerialChild();
        test.tryDeSerialChild();
        test.trySerialInstance();
        test.tryDeSerialInstance();
    }

    public static class Child extends WriteReplace{

    }

    public static class WriteReplace implements Serializable{
        private int a;

        public void setA(int a) {
            this.a = a;
        }

        public int getA() {
            return a;
        }

        private Object writeReplace() throws ObjectStreamException{
            return new Common();
        }
    }

    public static class Common implements Serializable{

        private transient int a;

        public  Common(){
            this.a = 1;
        }

        public int getA() {
            return a;
        }

        private void writeObject(java.io.ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(a);
        }

        private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
            in.defaultReadObject();
            in.readInt();
            System.out.println("Common readObject has been invoked");
        }

    }

    public static class SingleInstance implements Serializable{

        public static SingleInstance INSTANCE = new SingleInstance();

        private transient int a;

        private  SingleInstance(){
            this.a = 1;
        }

        public int getA() {
            return a;
        }

        private void writeObject(java.io.ObjectOutputStream out) throws IOException {
            out.defaultWriteObject();
            out.writeInt(a);
        }

        private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
            in.defaultReadObject();
            in.readInt();
            System.out.println("SingleInstance readObject has been invoked");
        }

        private Object readResolve() throws ObjectStreamException{
            return INSTANCE;
        }
    }
}

运行结果

Common readObject has been invoked
th.algorithm.serial.SerializableReplaceTest$Common@27d6c5e0
Common readObject has been invoked
th.algorithm.serial.SerializableReplaceTest$Common@3feba861
th.algorithm.serial.SerializableReplaceTest$SingleInstance@5b480cf9
SingleInstance readObject has been invoked
th.algorithm.serial.SerializableReplaceTest$SingleInstance@5b480cf9
  • 如何理解 serialVersionUID

    serialVersionUID 既可以自动创建, 也可以手动赋值.其目的在于判断序列化和反序化的对象对应的类是否兼容.如果对应的serialVersionUID不同, 则在反序列化的过程中抛出 InvalidClassException.

    通过在序列化类中以如下方式定义 field , 可以手动指定 serialVersionUID

 ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

需要注意的是, 由于自动计算 serialVersionUID 的方式对类的变化高度敏感, 甚至某种程度上会依赖于 Java compiler, 所以手动指定一个 serialVersionUID 是非常有必要的.并且尽可能定义为 private .毕竟这不是一个能被子类继承的成员.对于 Array classes 而言, 就放弃治疗吧, 无法手动指定, 数组类存在默认计算的 serialVersionUID, 虽然它们并不会在匹配的时候用到.

例子

按照注释操作, 可以获取到异常

package th.algorithm.serial;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * ===================================================
 * 首先使用现有代码生成一个序列化后的文件
 * 然后恢复注释掉的部分(忽略掉手动指定uid的那部分代码)
 * , 并注释掉 main函数中
 * 序列化的部分, 并保留反序列化的部分, 运行.
 * ====================================================
 */

public class SerializableUidTest {

    private void trySerial(){
        try {
            File file = new File("algorithm/files/uid.tmp");
            if(file.exists())
                file.delete();
            file.createNewFile();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            oos.writeObject(new Uid());
            oos.flush();
            oos.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    private void tryDeSerial(){
        try {
            File file = new File("algorithm/files/uid.tmp");
            if(file.exists()) {
                ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
                Object object = ois.readObject();
                System.out.println(object);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args){
        SerializableUidTest test = new SerializableUidTest();
//        test.trySerial();
        test.tryDeSerial();
    }

    public static class Uid implements Serializable{

//        private static final long serialVersionUID = 42L;

        private int a;
        private int c;

        public Uid(){
            this.a = 6;
        }
    }
}

反序列化后的异常

java.io.InvalidClassException: th.algorithm.serial.SerializableUidTest$Uid; local class incompatible: stream classdesc serialVersionUID = 4997188412835492989, local class serialVersionUID = 7081360090031566712

可以看出, UID发生了明显的变化.若手动指定UId, 重复上述操作, 则不会出现异常.

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

推荐阅读更多精彩内容