对象序列化与反序列化

“最好的教材就是源码注释,然后是大牛的总结。”

从今天开始写博客,目的很明确,梳理零碎的java知识,总结并记录下来,日后可以快速回顾。比较适合步入中年的程序员,记性衰退。。。

先简单说一下个人学习心法:

(1)阅读JDK类注释,可以了解一个大概。

(2)查看关键的几个方法;

(3)搜一些源码解读的文章;

(4)找一下大牛的精华总结。

(5)有问题,及时对号入座,记录下来。

以下纯属个人摘录,如有侵权还请及时告知。有写的不对的地方或者更深入的见解也可以留言讨论。先在此谢过了(^_^)

当两个Java进程进行远程通信时,一个进程能否把一个Java对象发送给另一个进程呢?答案是肯定的。不过,发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。

把Java对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为Java对象的过程称为对象的反序列化。

对象的序列化与反序列化

当程序运行时,程序所创建的各种对象都位于内存中,当程序运行结束,这些对象就结束生命周期。

对象的序列化主要有两种用途:(1)把对象的字节序列永久的保存到硬盘上,通常存放在一个文件中。(2)远程通信传输,或在网络上传送对象的字节序列。


实现对象的序列化,必须实现Serializable接口或Externalizable接口,Externalizable接口继承自Serializable接口。

All subtypes of a serializable class are themselves serializable

可序列化类的子类是可序列化的。

DataInput从二进制流中重新构造任何Java基本类型形式的数据。

DataOutput将任何Java基本类型的数据转换为字节序列,并写入二进制流。

ObjectStreamClass是Serializable的类描述器,更多相关的描述可以查这个

https://docs.oracle.com/javase/6/docs/platform/serialization/spec/serialTOC.html

#JDK类库中的序列化API

java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化成一个对象,并将其返回。

JDK类库中的序列化API只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则ObjectOutputStream的writeObject(Object obj)方法会抛出IOException。实现Serializable或Externalizable接口的类也称为可序列化类。Externalizable接口继承自Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式。JDK类库中的部分类(如String类、包装类和Date类等)都实现了Serializable接口。

对象的序列化主要包括以下步骤。

1)创建一个对象输出流,它可以包装一个其他类型的目标输出流,比如文件输出流:

ObjectOutputStream out=new ObjectOutputStream(new fileOutputStream("D:\\objectFile.obj"));

(2)通过对象输出流的writeObject()方法写对象:

out.writeObject("hello");  //写一个String对象

out.writeObject(new Date());  //写一个Date对象

JDK类库中的序列化API对象的反序列化主要包括以下步骤。

(1)创建一个对象输入流,它可以包装一个其他类型的源输入流,比如文件输入流:

ObjectInputStream out=new ObjectInputStream(new FileInputStream("D:\\objectFile.obj"));

(2)通过对象输入流的readObject()方法读取对象:

String obj1=(String)out.readObject();  //读取一个String对象Date obj2=(Date)out.readObject();  //读取一个Date对象

为了能读出正确的数据,必须保证向对象输出流写对象的顺序与从对象输入流读对象的顺序一致

先向objectFile.obj文件写入三个对象和一个int类型的数据,然后再依次把它们从文件中读入到内存中。

public static void main(String[] args) throws IOException, ClassNotFoundException {

        String file = "/Users/xjc/test/tmp/objectFile.obj";

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));

        String obj1 = "hello";

        Date obj2 = new Date();

        Customer obj3 = new Customer("Tom", 20);

        // 序列化对象

        out.writeObject(obj1);

        out.writeObject(obj2);

        out.writeObject(obj3);

        out.writeInt(123);

        // 写入基本类型的数据

        out.close();

        // 反序列化对象

        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));

        String obj11 = (String) in.readObject();

        System.out.println("obj11:" + obj11);

        System.out.println("obj11==obj1:" + (obj11.equals(obj1)));

        Date obj22 = (Date) in.readObject();

        System.out.println("obj22:" + obj22);

        System.out.println("obj22==obj2:" + (obj22.equals(obj2)));

        Customer obj33 = (Customer) in.readObject();

        System.out.println("obj33:" + obj33);

        System.out.println("obj33:" + obj3);

        System.out.println("obj33==obj3:" + (obj33 == obj3 ));

        int var = in.readInt();  //读取基本类型的数据

        System.out.println("var:" + var);

        System.out.println(obj33.toString());

        in.close();

    }

}

输出如下:

obj11:hello

obj11==obj1:true

obj22:Mon May 07 19:39:28 CST 2018

obj22==obj2:true

obj33:{Tom,20}

obj33:{Tom,20}

obj33==obj3:false

var:123

按照默认方式序列化时,如果由一个ObjectOutputStream对象多次序列化同一个对象,那么由一个ObjectInputStream对象反序列化出来的也是同一个对象。

实现Serializable接口ObjectOuputStream只能对实现了Serializable接口的类的对象进行序列化。默认情况下,ObjectOuputStream按照默认方式序列化,这种序列化方式仅仅对对象的非transient的实例变量进行序列化,而不会序列化对象的transient的实例变量,也不会序列化静态变量。

例Customer1类中,定义了一些静态变量、非transient的实例变量,以及transient的实例变量。

public class Customer implements Serializable {

private static int count;  //用于计算Customer对象的数目

private static final int MAX_COUNT=1000;

private String name;

private transient String password;..

}

当ObjectInputStream按照默认方式反序列化时,有以下特点:

如果在内存中对象所属的类还没有被加载,那么会先加载并初始化这个类。如果在classpath中不存在相应的类文件,那么会抛出ClassNotFoundException;

在反序列化时不会调用类的任何构造方法;

如果一个实例变量被transient修饰符修饰,那么默认的序列化方式不会对它序列化。根据这一特点,可以用transient修饰符来修饰以下类型的实例变量:(1)实例变量不代表对象的固有的内部数据,仅仅代表具有一定逻辑含义的临时数据。(2)实例变量表示一些比较敏感的信息(比如银行账户的口令),出于安全方面的原因,不希望对其序列化。(3)实例变量需要按照用户自定义的方式序列化,比如经过加密后再序列化。在这种情况下,可以把实例变量定义为transient类型,然后在writeObject()方法中对其序列化。

序列化对象图类与类  之间 可能存在  关联关系  。以下代 码创建的三个对象之间的关联关系 。//客户Tom有两个  订单 ,订单编号分别为“number1”和“number2”

Customer2 customer=new Customer2("Tom");

Order2 order1=new Order2("number1",customer);

Order2 order2=new Order2("number2",customer);

customer.addOrder(order1);

customer.addOrder(order2);


当通过ObjectOutputStream对象的writeObject(customer)方法序列化Customer2对象时,会不会序列化与它关联的Order2对象呢?答案是肯定的。

在默认方式下,对象输出流会对整个对象图进行序列化。当程序执行writeObject(customer)方法时,该方法不仅序列化Customer2对象,还会把两个与它关联的Order2对象也进行序列化。

当通过ObjectInputStream对象的readObject()方法反序列化Customer2对象,实际上会对整个对象图反序列化。

按照默认方式序列化对象A时,实际上被序列化的对象图中包括:对象A、对象B、对象C、对象D、对象E、对象F和对象G。


如果用户希望控制类的序列化方式,可以在可序列化类中提供 以下形式的writeObject()方法和readObject()方法

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

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

当ObjectOutputStream对一个Customer对象进行序列化时,如果该Customer对象具有writeObject()方法,那么就会执行这一方法,否则就按默认方式序列化。在Customer对象的writeObject()方法中,可以先调用ObjectOutputStream的defaultWriteObject()方法,  使得对象输出流先  执行默认的序列化操作

当ObjectInputStream对一个Customer对象进行反序列化时,如果该Customer对象具有readObject()方法,那么就会执行这一方法,否则就按默认方式反序列化。在Customer对象的readObject()方法中,可以先调用ObjectInputStream的defaultReadObject()方法, 使得对象输入流先执行默认的反序列化操作

在以下情况,可以考虑采用用户自定义的序列化方式,从而控制序列化的行为:

(1)确保序列化的安全性,对敏感的信息加密后再序列化,在反序列化时则需要解密

(2)确保对象的成员变量符合正确的约束条件

(3)优化序列化的性能

(4)便于更好 的封装类的内部数据结构,确保类的接口不会被类的内部实现所束缚

#实现Externalizable接口

Externalizable接口继承自Serializable接口。如果一个类实现了Externalizable接口,那么将完全由这个类控制自身的序列化行为。Externalizable接口中声明了两个方法:public void writeExternal(ObjectOutput out)throws IOException public void readExternal(ObjectInput in) throws IOException,ClassNotFoundException

使用Externalizable进行序列化时,当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。

writeExternal()方法负责序列化操作,readExternal()方法负责反序列化操作。在对实现了Externalizable接口的类的对象进行反序列化时,会先调用类的不带参数的构造方法,这是有别与默认反序列化方式的。由此可见,一个类如果实现了Externalizable接口,那么它必须具有public类型的不带参数的构造方法,否则这个类无法反序列化

#可序列化类的不同版本的序列化

兼容性假定Customer5类有两个版本1.0和2.0,如果要把基于1.0的序列化数据反序列化为2.0的Customer5对象,或者把基于2.0的序列化数据反序列化为1.0的Customer5对象,会出现什么情况呢?如果可以成功的反序列化,则意味着 不同版本之间 对序列化兼容 ,反 之,则 意味着 不同版本之间对序列化不兼容。

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态常量:

private static final long serialVersionUID;

以上serialVersionUID的取 值是Java运行时  环境根据类的内部细节自动生成的。如果对类的源代码做了修 改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化

类的serialVersionUID的默认值依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显式的定义serialVersionUID,为它赋予明确的值。显式的定义serialVersionUID有两种用途:(1)在某些场合,希望 类的不同版本对序列化兼容 ,因此需要确保类的不同版本具有相同的serialVersionUID。(2)在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

为什么一个类实现了Serializable接口,它就可以被序列化呢?在上节的示例中,使用ObjectOutputStream来持久化对象,在该类中有如下代码:

private void writeObject0(Object obj, boolean unshared) throws IOException {      ...   

            // remaining cases

            if (obj instanceof String) {

                writeString((String) obj, unshared);

            } else if (cl.isArray()) {

                writeArray(obj, desc, unshared);

            } else if (obj instanceof Enum) {

                writeEnum((Enum) obj, desc, unshared);

            } else if (obj instanceof Serializable) {

                writeOrdinaryObject(obj, desc, unshared);

            } else {

                if (extendedDebugInfo) {

                    throw new NotSerializableException(

                        cl.getName() + "\n" + debugInfoStack.toString());

                } else {

                    throw new NotSerializableException(cl.getName());

                }

            }

...

}

从上述代码可知,如果被写对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。

另外,保持单例的特性的例子很好,实质就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。

引自:

http://developer.51cto.com/art/201202/317181.htm

总结如下:

1. 对象序列化有三种方式:

一实现Serializable接口,使用默认序列化方式,可序列化非transient非静态变量(静态变量是类共享的,会改变,加上final修饰符则可以序列化),不会调用该对象的构造器,但是会调用父类的构造器,如果父类没有默认构造器则会报错;

二实现Serializable接口,自定义private修饰的writeObject方法和readObject方法,在这两个方法里面需要使用stream.defaultWriteObject()序列化那些非static和非transient修饰的成员变量,static的和transient的变量则用stream.writeObject(object)显式序列化;

三实现Externalizable接口,必须实现writeExternal(ObjectOutput)和readExternal(ObjectInput)方法,实现反序列化时,首先会调用对象的默认构造器,然后再调用readExternal方法;

2.序列化版本兼容

serialVersionUID,Java运行时环境根据类的内部细节自动生成,受不同编译器影响,最好显示定义。

3.序列化范围为序列化对象图

重要的类:

Externalizable/Serializable

ObjectOutputStream/ObjectInputStream、

ObjectStreamClass、ObjectStreamConstants、

DataOutput/DataInput

参考资料:

Java Object Serialization Specification

https://docs.oracle.com/javase/6/docs/platform/serialization/spec/serialTOC.html

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

推荐阅读更多精彩内容