Java序列化机制

Java序列化机制


序列化和反序列化

Java序列化是Java内建的数据(对象)持久化机制,通过序列化可以将运行时的对象数据按照特殊的格式存储到文件中,以便在将来重建对象。

Java序列化是Java IO的一部分,需要ObjectStream提供支持。支撑序列化机制的基础是运行期对对象的信息(如类信息)和数据(如成员域)的读取和存储,从而将数据的字节流写入文件或字节数组中。反序列化过程则是读取字节流中对象的描述信息和数据,通过指定类的构造器和持久化的数据动态地重建出对象。

当然,对于一般对象而已,重建后的对象和被序列化到文件中的对象,不会是同一个了(地址),因为可能都不在同一台计算机中,序列化中的“内存地址”这个对象相等的标志,变成了对象的序列号,这也是序列化的名称由来。不过对于枚举对象,因为在反序列化过程做了特殊的处理,保证了对象的唯一性。

需要注意的是,序列化是保存对象的状态,和类状态无关,所以它不会读取静态数据。


序列化和反序列化的序列号

序列号是关联到对象的,是内部流处理的机制,外部是不可见的。

  1. 序列化
  • 序列化的每个对象对应一个序列号,这个序列号是唯一对应一个对象的,相当于对象内存地址的作用;
  • 对于每个首次遇到的对象,会将其数据存储到输出流中;
  • 对于相同的对象(这个相同的判断是通过地址),会引用之前的对象的序列号,相当于引用一个地址;
  1. 反序列化(这个过程序列号就是对象的唯一标识符了)
  • 对于首次遇到的序列号,会根据流中的信息构建一个Java对象;
  • 对于与之前的序列号相同的序列号,直接引用之前构建的对象(这里是对象的内存地址)

序列化的版本号

序列化的版本号是在类中定义的private static final long serialVersionUID,这个字段用来标示类的版本,它是数据域类型和方法签名信息通过SHA算法取到的指纹,采用了SHA码的前8个字节,主要用在反序列化过程中的类的校验。如果在反序列化过程中,当前类的serialVersionUID和序列化文件中的serialVersionUID不一致,那么就无法从流中构建对象,并抛出异常。
那么什么时候这个serialVersionUID会变呢?
主要有下面两种情况

  • 在实现Serializable接口时,没有定义serialVersionUID属性,但在序列化之后,修改了类结构;
  • 手动修改了serialVersionUID的值;

serialVersionUID是用来应对类在序列化之后类发生了变更的情况。对于第一个情况,在定义可序列化类时没有定义serialVersionUID,不会影响序列化过程,因为在序列化过程中会自动生成一个写入到流中,如果在之后没有修改类的任何域或方法,反序列化是没有问题的,(因为生成指纹的基础没变,自动生成的指纹即serialVersionUID是一致的)。但是如果在序列化之后修改了类,反序列化就会失败,除非手动加上serialVersionUID值和前面自动生成的值一致。查看旧值的方法是:使用jdk的命令serialver <类名>,能拿到他早期版本的版本号。

对于第二种情况,一般而言,除非是有此类需求,不然不会手动去破坏反序列化。


相关的类、接口

序列化标识接口

  • java.io.Externalizable
  • java.io.Serializable

序列化机制实现类

  • java.io.ObjectInputStream
  • java.io.ObjectOutputStream

实现Serializable接口或者Externalizable接口的才可以被序列化。Serializable接口没有任何方法,是一个标记接口,Externalizable接口是Serializable接口的子接口,拥有两个公共方法,用来自定义序列化过程。

Serializable和Externalizable的区别

实现了Externalizable接口的类,需要实现接口的两个抽象方法,readExternal和writeExternal,需要读写哪些数据都需要显式的调用流的读写方法,也就是序列化的任务从Java内建转移到了开发者,提供了更大自由度的同时,也提高了复杂度。而且在Externalizable反序列化时,会调用类的public无参构造器,如果类中没有定义无参构造器(没有重载构造器的话,会有默认的无参构造器的),会抛出异常。后面再详细说明此接口的使用。

对象流是序列化的核心,readObject和writeObject方法从文件中反序列化对象和将对象序列化到文件中。


使用默认的序列化

以下是实现了Serializable接口的类,main方法演示了对象的序列化和反序列化的使用。

public class SerializableUser implements Serializable {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    private SerializableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public SerializableUser() {
    }

    @Override
    public String toString() {
        return "User  -> name:" + name + " ,age=" + age;

    }

    public static void main(String[] args) throws IOException {
        SerializableUser user = new SerializableUser("harry", 19);
        SerialUtil.writeToFile("ser", user);
        SerializableUser u = (SerializableUser) SerialUtil.readFromFile("ser");
        System.out.println(u);
        // Files.deleteIfExists(Paths.get("ser"));
    }
}

// 工具类

public class SerialUtil {
    public static void writeToFile(String path, Object obj) {
        ObjectOutputStream oos;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(path));
            oos.writeObject(obj);
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Object readFromFile(String path) {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(path));
            return ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (ois != null)
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return null;

    }

结果:

User  -> name:harry ,age=19
序列化文件内容:
aced 0005 7372 0026 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 5365
7269 616c 697a 6162 6c65 5573 6572 0000
0000 0000 0001 0200 0249 0003 6167 654c
0004 6e61 6d65 7400 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b78 7000 0000
1374 0005 6861 7272 79

自定义序列化

想要修改Serializable接口实现类的序列化逻辑,比如有些字段不需要写入,或者自己想控制写入的过程,如想给写入文件的字节流加密等等,就不能直接使用对象流提供的read和write方法了。

transient关键字

使用transient关键字修饰的字段,在序列化时会被忽略,不会写入流中。transient意思是瞬时的,既然不是长久的,就代表不要持久化。

重写writeObject()和readObject()

在实现类中重写writeObject和readObject方法可以改变序列化的方法调用,会调用类中重写的方法,而不会再走对象流的方法,实现逻辑是在对象流的writeObject和readObject中通过反射判断类中是否重写了方法,然后反射调用。
一般重写的逻辑是先调用流的defaultWriteObject和defaultReadObject方法,然后追加自己的写入逻辑。这两个default方法是原本对象流的writeObject和readObject方法会调用的,所以具备默认的读写作用。可以参考ArrayList类的设计,ArrayList内部使用数组来存储元素,且数组是会动态扩容的,如果直接序列化数组,会序列化很多的空元素即null到流中,既浪费空间也降低了效率,所以在类中将数组变量标注成transient,然后在重写writeObject和readObect方法,将实际的元素写入对象流和从对象流中读出。

readResolve()

这个方法是应对enum出现之前设计的枚举的代替代码和单例代码的措施。
看一段示例:

public class SimulateEnum {

    /**
     * 模拟一个枚举类,在Java内建枚举出现之前的情况
     */
    static class MyEnum implements Serializable {
        private static final long serialVersionUID = 1L;
        private String name;
        private static int order = 0;
        private int ordernum;

        private MyEnum(String name) {
            this.name = name;
            this.ordernum = order++;
        }

        public String getName() {
            return name;
        }

        public int getOrdernum() {
            return ordernum;
        }

        public static final MyEnum A = new MyEnum("A");
        public static final MyEnum B = new MyEnum("B");
        public static final MyEnum C = new MyEnum("C");
    }

    public static void main(String[] args) throws IOException {
        MyEnum a = MyEnum.C;
        System.out.println(a.getName());
        System.out.println(a.getOrdernum());
        SerialUtil.writeToFile("enum", a);
        MyEnum rebuilda = (MyEnum) SerialUtil.readFromFile("enum");
        System.out.println(rebuilda.getName());
        System.out.println(rebuilda.getOrdernum());
        System.out.println(a == rebuilda);
        Files.deleteIfExists(Paths.get("enum"));
    }
}

在示例中我们模拟了一个枚举的实现,我们定义了私有的构造器和几个作为枚举对象的静态对象,我们期待的是枚举是安全的,不会再生成除了定义的三个变量之外的其他变量,但是反序列化过程会破坏这种设计。看结果:

C
2
C
2
false

因为对象的反序列化会创建新的对象的,即使类的构造器是私有的,这会破坏单例模式的设计。为了去保护在Java枚举出现之前的模拟枚举,对象留提供了一个解决措施——在类中实现readResolve方法。在readResolve方法中拦截新对象的生成,使之返回已有的对象。
我们在MyEnum类中加入以下方法:

private Object readResolve() throws Exception {
            switch (name) {
            case "A":
                return A;
            case "B":
                return B;
            case "C":
                return C;
            default:
                throw new Exception();
            }

        }

再执行返回的结果就是true了。
那么为什么在反序列真正的枚举的时候就不用再考虑在类中实现readReslove方法呢,原因是ObjectStream对枚举做了支持。看一下调用栈。

readObejct()-> readObject0()-> checkResolve(readEnum(unshared))->readEnum():

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

看一下源码,实际上和我们自己写的readResolve里面的意思是差不多的。

Externalizable接口

一个实现Externalizable接口的类表明它是可被外部化的,也是可序列化,只是这个序列化的责任转移给了外部控制。这个接口的定义:

public interface Externalizable extends java.io.Serializable {
    
    void writeExternal(ObjectOutput out) throws IOException;

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

两个抽象方法给开发者自行实现序列化的处理。
示例:

public class ExternalizableUser implements Externalizable {

    private String name;
    private int age;

    private ExternalizableUser(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public ExternalizableUser() {
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }

    @Override
    public String toString() {
        return "User  -> name:" + name + " ,age=" + age;

    }

    public static void main(String[] args) throws IOException {
        ExternalizableUser user = new ExternalizableUser("harry",19);
        SerialUtil.writeToFile("ext", user);
        ExternalizableUser u = (ExternalizableUser) SerialUtil.readFromFile("ext");
        System.out.println(u);
        Files.deleteIfExists(Paths.get("ext"));
    }
}
//User  -> name:harry ,age=19

如果上面示例中的writeExternal和readExternal方法没有实现,那么返回的对象是一个空的对象,数据是默认值。
序列化的文件内容是:

aced 0005 7372 0028 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 4578
7465 726e 616c 697a 6162 6c65 5573 6572
c8f9 50ef 76db 2f15 0c00 0078 7074 0005
6861 7272 7977 0400 0000 1378 

感兴趣的同学可以去研究一下序列化文件的格式。

The end

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

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,825评论 0 24
  • java的序列化机制支持将对象序列化为本地文件或者通过网络传输至别处, 而反序列化则可以读取流中的数据, 并将其转...
    Ten_Minutes阅读 384评论 0 1
  • 张小凡蹲在地上扔石头,他记得自己穿越之前是在泡澡,用绿茶泡澡,清香解乏。泡着泡着他就睡着了!一睁眼,他就发现自己穿...
    哇呵呵a阅读 394评论 0 2
  • 2018.4.11 【阅读打卡】 Day48---【阅读一小时】 时间管理四象限: 第一优先紧急重要工作 第二优先...
    Karen娟儿阅读 247评论 0 1
  • 七律·丰收(新韵) 贺信莺声喜气扬,潮升雪化涨春江; 起飞雏燕戏田地,黄透枇杷缀麦场; 墩就高冈颗粒小,集成经典字...
    補缺楼丨胡德棒阅读 1,599评论 0 2