Java 串行化

Serialization 是把对象的状态转换为字节流,同时字节流也可以转换为对象,反向过程叫做 Deserialization

串行化可以把对象的状态保存到文件中,也可以通过网络传输对象

串行化接口

java.io.Serializable接口是一个标记接口(不含有数据和方法),String和所有的原始数据类型的包装器类都默认实现了该接口

ObjectOutputStream类用来串行化对象为OutputStream,类字段如果是引用,对应的引用对象也需要序列化
ObjectInputStream 类用来反序列化先前串行化的原始数据和对象,重构对象

可以写入多个对象或原始数据类型到输出流,这些对象必须从相应的ObjectInputstream读取,类型和顺序应该要和写入的相同

FileOutputStream fos = new FileOutputStream("t.tmp");
ObjectOutputStream oos = new ObjectOutputStream(fos);

oos.writeInt(12345);
oos.writeObject("Today");
oos.writeObject(new Date());

oos.close();
FileInputStream fis = new FileInputStream("t.tmp");
ObjectInputStream ois = new ObjectInputStream(fis);

int i = ois.readInt();
String today = (String) ois.readObject();
Date date = (Date) ois.readObject();

ois.close();
  • 可以使用try/catch,处理转换过程中可能出现的异常
  • 使用transient修饰符的字段,不会被序列化
  • 静态字段不会序列化(serialVersionUID例外)
  • 不是每个类都可序列化,有些类是不能序列化的, 例如涉及线程的类
  • 子类实现Serializable接口而父类未实现时,父类不会被序列化
  • 父类实现序列化,子类自动实现序列化

如果需要特殊处理序列化和反序列化,可以在类中自定义序列化方法

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

为了避免因为,JAVA的序列化机制采用了一种特殊的算法:

1、所有保存到磁盘中的对象都有一个序列化编号
2、当程序试图序列化一个对象时,会先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化,系统才会将该对象转换成字节序列并输出
3、如果对象已经被序列化,程序将直接输出一个序列化编号,而不是重新序列化

serialVersionUID

用来确保在反序列化的过程中,加载的是同样的类(序列号对应的类)

语法:

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

原因:
有可能序列化一个对象到文件中,几个月后才在不同的JVM进行反序列化,此时对应的类可能已经改变了。
如果要反序列化的serialVersionUID不相同,产生异常InvalidClassException

生成方式:

  • 显式声明,比如和系统的版本保持一致
  • 自动生成,没有显式声明的时候,进行序列化的时候根据相关规则产生一个默认的serialVersionUID,但是相应的计算对类的细节非常敏感,可能编译器的实现而有所不同,所以强烈建议所有可序列化的类都明确声明serialVersionUID

Externalizable

Serializable接口的子类,通过特定的两个方法来指定要序列化的对象,而父类直接序列化所有对象

writeExternal(ObjectOutput out)
readExternal(ObjectInput in)

与父类Serializable的区别,反序列化重构对象时,先通过一个public的无参数构造函数创建对象,再调用readExternal方法,父类是直接通过ObjectInputStream创建的


测试

import java.io.*;

public class Solution {
    public static void main(String[] args) throws IOException {
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/home/wdy/Desktop/test"));
        outputStream.writeObject(new Test(0xBBBBBBBB,"Wang"));
        outputStream.close();
    }

    public static class Test implements Serializable {
        public static final long serialVersionUID = 0xAAAAAAAAAAAAAAAAL;
        int num;
        String name;

        public Test(int num, String name) {
            this.num = num;
            this.name = name;
        }
    }
}

写出的二进制数据:

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: AC ED 00 05 73 72 00 21 63 6F 6D 2E 67 69 74 68    ,m..sr.!com.gith
00000010: 75 62 2E 77 61 6E 67 64 79 31 32 2E 53 6F 6C 75    ub.wangdy12.Solu
00000020: 74 69 6F 6E 24 54 65 73 74 AA AA AA AA AA AA AA    tion$Test*******
00000030: AA 02 00 02 49 00 03 6E 75 6D 4C 00 04 6E 61 6D    *...I..numL..nam
00000040: 65 74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53    et..Ljava/lang/S
00000050: 74 72 69 6E 67 3B 78 70 BB BB BB BB 74 00 04 57    tring;xp;;;;t..W
00000060: 61 6E 67                                           ang


解释:

序列化会记录每个类的名称,字段的名称和类型,最后才是具体的数据类型

ObjectOutputStream初始化时就会写出头信息:

  • 写出AC ED表示魔数,即文件类型,写出版本号00 05,这两个字段都是固定的

写一个普通的对象writeOrdinaryObject

  • 对象标志0x73

类描述信息writeClassDesc

  • 类标志0x72,表示一个新的类描述
  • 类名称(modified-utf-8格式,这里编码长度为33字节即0x21,大字节序写出为short形式,之后是UTF内容,这里全部是单字节编码com.github.wangdy12.Solution$Test),序列号(8个0xAA
  • 标志,一个字节,表示不同的序列化类型(例如对象是否自定义了writeObject方法),这里为0x02
  • 字段数目,两个字节,这里为00 02,即两个字段,接下来处理每个字段的描述信息
  • int字段的写出,类型占用一个字节,对应为I,即49,接下来是字段名称,前两个字节表示长度00 03,后面是具体的名称num
  • String字段的写出:签名第一个字符对应为L,即0x4C,然后是字段名称,前两个字节表示长度00 04,后面是字段名称name,如果不是原始类型,再写出其类型签名,这里是Ljava/lang/String;,以writeString写出其类型签名,先写标志0x74,然后内部再调用writeUTF以UTF形式写出,长度0x12,即18,内容Ljava/lang/String;
  • 对象块的结束标志0x78
  • 递归写出对象的父类信息描述,递归调用writeClassDesc,这里为null,写出0x70

写出具体的数据writeSerialData

  • 如果有自定义的writeObject就调用,如果没有使用默认的defaultWriteFields写出,先写出原始数据类型的值,然后写出对象字段中的实际数据

序列化使用的常量位于ObjectStreamConstants类中,内部包含一些标志位

static final short STREAM_MAGIC = (short)0xaced;
static final short STREAM_VERSION = 5;
static final byte TC_NULL =         (byte)0x70;
static final byte TC_CLASSDESC =    (byte)0x72;
static final byte TC_OBJECT =       (byte)0x73;
static final byte TC_STRING =       (byte)0x74;
static final byte TC_ENDBLOCKDATA = (byte)0x78;

Kryo

一种更高效的的序列化方式,相同对象的序列化,大小大大减小

public class Solution {
    public static void main(String[] args) throws IOException {
        Kryo kryo = new Kryo();
        kryo.register(Test.class);//需要进行注册,不注册时改为 kryo.setRegistrationRequired(false);
        Test test = new Test(0xBBBBBBBB,"Wang");
        Output output = new Output(new FileOutputStream("/home/wdy/Desktop/test-kryo"));
        kryo.writeClassAndObject(output, test);
        output.close();

        Input input = new Input(new FileInputStream("/home/wdy/Desktop/test-kryo"));
        Object object2 = kryo.readClassAndObject(input);
        input.close();
        System.out.println(((Test)object2).num);
    }
}

注册后序列化结果只有10个字节,不包含类型信息,第一个字节是一个变长int,表示注册对应的序号,之后四个字节表示Wang,且g最后一个字节的最高位为1,即最后一个字节为负数,表示字符串结束,最后五个字节是一个边长编码的int,即0xBBBBBBBB

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: 0B 57 61 6E E7 89 91 A2 C4 08                      .Wang.."D.

如果不进行注册,对应结果会记录类名称

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: 01 00 63 6F 6D 2E 67 69 74 68 75 62 2E 77 61 6E    ..com.github.wan
00000010: 67 64 79 31 32 2E 53 6F 6C 75 74 69 6F 6E 24 54    gdy12.Solution$T
00000020: 65 73 F4 57 61 6E E7 89 91 A2 C4 08                estWang.."D.

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

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,834评论 0 24
  • 一、 序列化和反序列化概念 Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化de...
    步积阅读 1,434评论 0 10
  • 如果你只知道实现 Serializable 接口的对象,可以序列化为本地文件。那你最好再阅读该篇文章,文章对序列化...
    jiangmo阅读 456评论 0 2
  • 官方文档理解 要使类的成员变量可以序列化和反序列化,必须实现Serializable接口。任何可序列化类的子类都是...
    狮_子歌歌阅读 2,385评论 1 3
  • 你好!做梦也想不到我把信写到五线谱上吧? 五线谱是偶然来的,你也是偶然来的。不过我给你的信值得写在五线谱里呢.。但...
    Carina____阅读 99评论 0 0