认识Serializable和重写读写方法

简述

需要将对象持久化到我们的存储设备上或者通过网络传输到其他客户端,那么我们就需要序列化。

Serializable为Java中带的一种序列化方式,使用起来非常简单。只需要让类实现一个Serializable接口就可以进行序列化了。

使用

先讲初步使用,后面会讲到serialVersionUID。

  1. 定义一个User类,实现Serializable接口
public class User implements Serializable {
    private String name;
    private int age;

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

    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 String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

  1. 操作User类,持久化到cache.txt文件中,并从文件中读出来
//创建到User类
        User user = new User("大灰狼", 18);
        try {
            File file = new File("cache.txt");
            if (!file.exists()) {
                file.createNewFile();
            }

            //将User类序列化到文件中
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
            objectOutputStream.writeObject(user);

            System.out.println("序列化成功");

            //从文件中读出来
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
            User user1 = (User) objectInputStream.readObject();
            System.out.println("" + user1);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

就这样,最简单到使用就完成了。

serialVersionUID

如果,咱们某一天改了User类的内部结构,如增加了一个字段:boolean isMan,但是文件类已经持久化的数据并没有改动,我们需要反序列化读出来

  • 读取代码
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
            User user1 = (User) objectInputStream.readObject();
            System.out.println("" + user1);

我们会很明显的看到报出一个错误:

java.io.InvalidClassException: serial.User; local class incompatible: stream classdesc serialVersionUID = -3951066851478225822, local class serialVersionUID = -8381808064053356693
  • 原因:因为咱们存储的时候,有帮我们自动生成一个serialVersionUID就是-3951066851478225822,可以当作存储在文件中的类的版本标示。而我们本地类版本升级了,自动帮我们修改了版本标示。在反序列化的时候会检测这个版本标示是否相同,所以在反序列化的时候就通不过了,这时候就不能反序列化了。

我们自然不想看到这种情况出现,所以通常情况下,我们应该手动指定serialVersionUID的值,如123L

只需要在User中添加:

private static final long serialVersionUID = 123L;

那我们重新试试(前面的文件不能用了,删除后再来).

  1. 先写文件,只包含name和age。

  2. 然后我们再升级User类,在其中添了两个字段如下:

private boolean isMan;
private String address;
  1. 再读一次文件,打印如下
User{name='大灰狼', age=18, isMan=false, address='null'}

可以看到能成功读取了,只是缺失字段使用了默认值代替,做到了最大限度的恢复数据。

注意:类升级指的是内部结构,如果改了类名,那是怎么都反序列化不回来的,因为类结构有了毁灭性的改变,那错误将是这样的:
java.lang.ClassNotFoundException: serial.User

重写序列化和反序列化

就如同我们调用一样,序列化过程其实就是通过ObjectOutputStream的writeObject、readObject来体现的。我们跟踪下流程,可以看到调用到了要序列化对象的私有方法writeObject,部分代码如下:

  1. 从ObjectOutputStream进行写方法
public final void writeObject(Object obj) throws IOException {
    try {
           //调用该方法执行写
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }
  1. writeObject0:
private void writeObject0(Object obj, boolean unshared){
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                Class<?> repCl;
                //查找类信息,这里明显使用到了反射来查找
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
            }
}
  1. ObjectStreamClass.lookup
static ObjectStreamClass lookup(Class<?> cl, boolean all) {

            try {
            //创建流对象
                entry = new ObjectStreamClass(cl);
            } catch (Throwable th) {
                entry = th;
            }
}
  1. 创建流对象,寻找到要序列化对象中的私有方法,得到writeObjectMethod和readObjectMethod
private ObjectStreamClass(final Class<?> cl) {

//获取对象中的私有方法,writeObject和readObject
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
                        readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
}
  1. 在执行写序列化数据时调用到方法:writeSerialData
private void writeSerialData(Object obj, ObjectStreamClass desc)
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    //执行到了创建好流对象的方法
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }
}
  1. invokeWriteObject调用到了writeObjectMethod.invoke
void invokeWriteObject(Object obj, ObjectOutputStream out)
        throws IOException, UnsupportedOperationException
    {
    if (writeObjectMethod != null) {
            try {
            //通过反射执行了要序列化对象的writeObject方法
                writeObjectMethod.invoke(obj, new Object[]{ out });
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof IOException) {
                    throw (IOException) th;
                } else {
                    throwMiscException(th);
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }

到这里我们就能看到我们要序列化对象的writeObject也被调用了,readObject过程分析也是一样的。通过这样我们可以想象,是不是我们就可以自己实现这个方法来重写写数据呢?答案肯定是的,哈哈哈.

重写writeObject和readObject:
  1. 改isMan默认不序列化
  2. 重写写和读方法
public class User implements Serializable {

    private static final long serialVersionUID = 123L;

    private String name;
    private int age;
    private transient boolean isMan;
    private String address;

    public User(String name, int age, boolean isMan, String address) {
        this.name = name;
        this.age = age;
        this.isMan = isMan;
        this.address = address;
    }

    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;
    }

    public boolean isMan() {
        return isMan;
    }

    public void setMan(boolean man) {
        isMan = man;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", isMan=" + isMan +
                ", address='" + address + '\'' +
                '}';
    }

    /**
     * 重写写方法
     *
     * @param oos
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream oos) throws IOException {
        //调用默认的序列化方法,如里面注释一样,可以把非静态和非transient字段给序列化了
        oos.defaultWriteObject();
        //把isMan也序列化一下
        oos.writeBoolean(isMan);
        System.out.println("序列化成功");
    }

    /**
     * 重写读方法
     *
     * @param ois
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream ois) throws IOException,
            ClassNotFoundException {
        //调用默认的反序列化方法,如里面注释一样,可以把非静态和非transient字段给反序列化了
        ois.defaultReadObject();
        //读取序列化的字段
        isMan = ois.readBoolean();
        System.out.println("反序列化成功");
    }
}

再进行序列化和反序列化,就能看到isMan咱们也能够正常写入和读了。

序列化是持久化的一个必备品,记录到这里大概对Serializable就更熟悉了。

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

推荐阅读更多精彩内容