资讯专栏INFORMATION COLUMN

从java的序列化和反序列化说起

whlong / 1529人阅读

摘要:从的序列化和反序列化说起序列化是将对象的状态信息转换为可以存储或传输的形式的过程,而相反的过程就称为反序列化。当使用接口来进行序列化与反序列化的时候需要开发人员重写与方法。

从java的序列化和反序列化说起

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程,而相反的过程就称为反序列化。

在java中允许我们创建可复用的对象,但是这些对象仅仅存在jvm的堆内存中,有可能被垃圾回收器回收掉而消失,也可能随着jvm的停止而消失,但是有的时候我们希望这些对象被持久化下来,能够在需要的时候重新读取出来。比如我们需要在网络中传输对象,首先就需要把对象序列化二进制,然后在网络中传输,接收端收到这些二进制数据后进行反序列化还原成对象,完成对象的网络传输,java的序列化和反序列化功能就可以帮助我们现实此功能。

那么java要怎么样才能实现序列化和反序列化呢?

Serializable接口

在java中要实现序列化和和反序列化只需要实现Serializable接口,任何视图将没有实现此接口的对象进行序列化和反序列化操作都会抛出NotSerializableException,下面是实现:

public  byte[] serializer(T obj) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
    } catch (IOException e) {
        logger.error("java序列化发生异常:{}",e);
        throw new RuntimeException(e);
    }finally{
        try {
            if(oos != null)oos.close();
        } catch (IOException e) {
            logger.error("java序列化发生异常:{}",e);
        }
    }
    return baos.toByteArray();
}

public  T deserializer(byte[] data, Class clazz) {
    ByteArrayInputStream bais = new ByteArrayInputStream(data);
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(bais);
        return (T)ois.readObject();
    } catch (Exception e) {
        logger.error("java反序列化发生异常:{}",e);
        throw new RuntimeException(e);
    }finally{
        try {
            ois.close();
        } catch (IOException e) {
            logger.error("java反序列化发生异常:{}",e);
            throw new RuntimeException(e);
        }
    }
}

transient关键字

正常情况下,在序列化过程中,对象里面的属性都会被序列化,但是有的时候,我们想过滤掉某个属性不要被序列化,该怎么办呢,很简单java给我们提供了一个关键字来实现:transient,只要被transient关键字修饰了,就会被过滤掉

readObject和writeObject方法

在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。

如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

细心的你肯定也发现了,我们在序列化的类里面定于了这两个方法,但是并没有显式的调用这两个方法,那到底是谁调用的,又是何时被调用的呢?

深入ByteArrayOutputStream类源码会发现其调用栈:

ObjectOutputStream.writeObject(Object obj)----------->writeObject0(Object obj, boolean unshared)----------->writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared)----------->writeSerialData(Object obj, ObjectStreamClass desc)

在writeSerialData方法里面会先获取序列化类里面是否有writeObject(ObjectOutputStream out),有就会反射的调用,没有就执行默认的序列化方法defaultWriteFields(obj, slotDesc)。

ByteArrayInputStream也是同样的原理。

如果您读过在ArrayList的源码,你可能会发现在ArrayList中的字段elementData被关键字transient修饰了,而elementData字段是ArrayList存储元素的,难道ArrayList存储的元素序列化会被忽略吗?但是你会发现并没有被忽略,而是能正常的序列化和反序列化,这是为什么呢?答案就是,ArrayList写有上面提到的readObject和writeObject两个方法,ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为50,而实际只放了1个元素,那就会序列化49个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient,自定义序列化过程,这样可以优化存储。

Externalizable接口

除了Serializable 之外,java中还提供了另一个序列化接口Externalizable,继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。

序列化ID

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。

Protobuf

我们知道java自带的序列化效率是非常低的,因为它序列化生成的字节数非常多(包含了很多类的信息),不太适合用于存储和在网络上传输,下面来介绍下google给我们提供一个序列化效率相当高的框架protobuff,比起java原生的序列化出来的字节数小十几倍。那么它是如何做到的呢?

以int类型为例,int在java的占用4个字节,如果我们不做特殊处理,int类型的值转化成二进制也需要占用4个字节的空间,但是protobuff却不是这样做的,请看下面代码:

while (true) {
    if ((value & ~0x7F) == 0) {
        UnsafeUtil.putByte(buffer, position++, (byte) value);
        break;
    } else {
        UnsafeUtil.putByte(buffer, position++, (byte) ((value & 0x7F) | 0x80));
        value >>>= 7;
    }
}

value & ~0x7F 是什么意思呢?0x7F取反跟value相与,那么value的低7位全部被置0了,如果此时相与的值等于0,说明value的值不会大于0x7F=127,就可以用一个字节来表示,大大节省了字节数,看个列子:

value=0x00000067,转换成二进制:

0000 0000  0000 0000  0000 0000  0110 0111

& 1111 1111 1111 1111 1111 1111 1000 0000

= 0000 0000 0000 0000 0000 0000 0000 0000

此时value & ~0x7F=0,当把value强制转换成byte类型时,int会被截断,只剩下低位字节,于是当value值小于128时,序列化后的字节就变成:0110 0111,一个字节就可以表示了。

问题来了,如果value的值大于0x7F呢,接着看(value & 0x7F) | 0x80这句代码,假设value=2240,

 0000 0000  0000 0000  0000 1000  1100 0000     0x000008C0

& 0000 0000 0000 0000 0000 0000 0111 1111 0x0000007F

= 0000 0000 0000 0000 0000 0000 1100 0000 0x000000C0

| 0000 0000 0000 0000 0000 0000 1000 0000 0x00000080

= 0000 0000 0000 0000 0000 0000 1100 0000 0x000000C0

这个过程意思就是获取value的最低位字节,把这个字节的最高位置为1,表示后面还有可读字节。

对0x000000C0强转byte类型就变成:1100 0000,然后向右移7位:

0000 0000 0000 0000 0000 0000 0001 0001

重复上面的步骤,得到0001 0001,循环结束,最后得到:

1100 0000 0001 0001

2个字节就可以表示2240了,但是此时你会发现我们每次向右移动的是7位,移4次才能表示28位,但是int要占用32位,如果value的值比较大,假如等于2147483647,那么这是就需要5个字节来表示,综上所述protobuff表示一个int类型的值就不会固定4个字节,而是用1-5个字节动态来表示;那么你可能又会有疑问了,5个字节来表示一个int,字节数不是变多了么?其实从概率角度来看,我们业务上不能可能每一个int值都是一个非常大的值,所以还是可以为我们节省非常大的字节空间。同理long,double,float也是同样的原理。下面就以proptobuff3来介绍下protobuff的使用

一、整备

从protobuff官网下载protoc.exe可执行文件

二、编写proto文件,具体的语法参见官网文档

syntax = "proto3";
option java_package = "com.yanghui.serialize.protobuf3";
option java_outer_classname = "PersonModule";
message Person {
    int32 age = 1;
    int64 time = 2;
    string name = 3;
    map properties = 4;
}

三、编译成java类

e:/study/protobuf/bin/protoc.exe -I=D:/workspace/serialize/src/main/java/com/yanghui/serialize/protobuf3 --java_out=D:/workspace/serialize/src/main/java person.proto

-I:表示proto文件所在目录

--java_out:表示输出java的类

执行以上命令就会在指定的目录生成一个java类PersonModule.java,接下来就可以使用了

@Test
public void testProtobuffSerialize() throws InvalidProtocolBufferException {
    Builder builder = PersonModule.Person.newBuilder();
    builder.setAge(21);
    builder.setTime(100L);
    builder.setName("yanghui");
    builder.putProperties("key1", "value1");
    com.yanghui.serialize.protobuf3.PersonModule.Person person = builder.build();

    byte[] personBytes = person.toByteArray();
    System.out.println(Arrays.toString(personBytes));
    System.out.println(personBytes.length);

    com.yanghui.serialize.protobuf3.PersonModule.Person p = 
        com.yanghui.serialize.protobuf3.PersonModule.Person.parseFrom(personBytes);
    System.out.println(p.toString());
}


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76591.html

相关文章

  • java列化和反列化

    摘要:引语平时我们在运行程序的时候创建的对象都在内存中当程序停止或者中断了对象也就不复存在了如果我们能将对象保存起来在需要使用它的时候在拿出来使用就好了并且对象的信息要和我们保存时的信息一致序列化就可以解决了这样的问题序列化当然不止一种方式如下序 引语:     平时我们在运行程序的时候,创建的对象都在内存中,当程序停止或者中断了,对象也就不复存在了.如果我们能将对象保存起来,在需要使用它的...

    snowell 评论0 收藏0
  • java对象列化和反列化

    摘要:序列化对象和平台无关,序列化得到的字节流可以在任何平台反序列化。从文件中或网络上获得序列化的字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。因此意味着不要序列化静态变量不属于对象状态的一部分,因此它不参与序列化。 一.序列化和反序列化(1)序列化:将内存中的对象转化为字节序列,用于持久化到磁盘中或者通过网络传输。对象序列化的最主要的用处就是传递和保存对象,保证对...

    chadLi 评论0 收藏0
  • Java 列化和反列化

    摘要:把字节序列恢复为对象的过程称为对象的反序列化。代表对象输入流,它的方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。接口继承自接口,实现接口的类完全由自身来控制序列化的行为,而仅实现接口的类可以采用默认的序列化方式。 把对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为对象的过程称为对象的反序列化。    对象的序列化主要有两种用途:   1) 把...

    jcc 评论0 收藏0
  • springboot学习(三)——使用HttpMessageConverter进行http列化和反

    摘要:序列化反序列化主要体现在程序这个过程中,包括网络和磁盘。如果是开发应用,一般这两个注解对应的就是序列化和反序列化的操作。协议的处理过程,字节流内部对象,就涉及这两种序列化。进行第二步操作,也就是序列化和反序列化的核心是。 以下内容,如有问题,烦请指出,谢谢! 对象的序列化/反序列化大家应该都比较熟悉:序列化就是将object转化为可以传输的二进制,反序列化就是将二进制转化为程序内部的...

    stackfing 评论0 收藏0
  • Java IO (三) 读取其他进程数据,RandomAccessFile,列化和反列化

    摘要:虚拟机读取其他进程的数据对象的方法可以运行平台上的其他程序该方法产生一个对象对象代表由该程序启动启动的子进程类提供如下三个方法用于和其子进程通信获取子进程的错误流获取子进程的输入流获取子进程的输出流这里的输入流输出流容易混淆从程序的角度思考 Java虚拟机读取其他进程的数据 Runtime对象的exec方法可以运行平台上的其他程序,该方法产生一个Process对象,Process对象...

    zhangfaliang 评论0 收藏0

发表评论

0条评论

whlong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<