色谈Java序列化:女孩子慎入 - 第280篇_java

悟纤:师傅,最近我老是碰到一个异常:java.io.NotSerializableException

师傅:徒儿你这是没有序列化。

悟纤:序列化这是啥?为啥要序列化呢?

师傅:好了,咱们今天就来讲一讲。

       为了提高访问速度,我们会使用到缓存,比如memcached来缓存一些不频繁变化的数据。这时候,将对象存到缓存管理器之后,那么可能就会遇到如上徒儿说的异常了。

BTW:别人是转角遇到爱,爱徒是转角遇到异常。

       对于NotSerializableException在Java中很好解决,但是很多人却不知道这是why,今天师傅给你好好讲解一番,悟纤你可得长点心呐,把耳朵带上。

 

一、何为序列化

序列化:把对象转换为字节序列的过程。

反序列化:把字节序列恢复为对象的过程

       这里的概念可能有点不好理解。我们得清楚,JVM是可以处理对象的,在不序列化的情况下,是以对象的状态在虚拟机中的,这时候,如果我们要将对象保存到缓存Memcached中的话,其中JVM有自己的进程,而Memcached也有自己的进程,两个不同进程之间要通讯,这就有话说了。

JVM对Memcached说:来我把这个对象传给你,你给我缓存一下,之后我需要的时候在找你要。

Memcached对JVM说:老兄,不行呐,我不懂得如何处理Java对象呐。

       JVM对Memcached说:那我把对象处理为字节你总可以处理了吧。

       Memcached说JVM说:这个可以有,那给我传字节,我也给你返回字节,你自己转换为对象吧。

       JVM心里暗讽:你咋这么笨呐,好吧,优秀如你,我只能默默看着。

BTW:字节(Byte )是计算机信息技术用于计量存储容量的一种计量单位,作为一个单位来处理的一个二进制数字串,是构成信息的一个小单位。

「百度百科」:https://baike.baidu.com/item/字节/1096318?fr=aladdin

       悟纤:那师傅都有什么情况需要进行序列化呐?

       师傅:悟纤别急,听为师慢慢道来。

 

二、什么场景下需要序列化

当你想把内存中的对象状态保存到一个文件中或者数据库中时候;

当你想用套接字在网络上传送对象的时候;

当你想通过RMI(Remote Method Invocation:远程方法调用)传输对象的时候;

BTW:当要 跨进程跨网络传输对象的时候,这时候基本要序列化了。

       悟纤:师傅,你这波BTW骚气外露耶。

       师傅:(咳嗽)悟纤,你头又痒了吧。

       下面举几个具体的场景:

(1)对象储存到缓存中(这个大家经常使用到吧,memcached.set(key,value))。

(2)把一个对象写到文件中(这个待会咱们会来写个小栗子)。

 

三、如何序列化

       如何进行序列化呐?常见的有如下几种方式

3.1 Serializable接口

Serializable最常见就是实现这个序列化标识接口,说它是标识是因为它的接口源码中其实什么都没有定义,纯粹只是为了标识而使用,一个类只有实现了这个接口,我们才能对它进行序列化。

3.2Externalizable接口

Externalizable这个接口其实是继承了Serializable,它更加灵活一点,它里面定义了writeExternal和readExternal两个方法分别用于序列化和反序列化使用。通过这两个方法,我们可以自己决定需要序列化那些数据。如果对象中涉及到很少的属性需要序列化,大多数属性无需序列化,这种情况使用Externalizable接口是比较灵活的。

3.3 第三方序列化

       也可以使用第三方的序列化进行操作,比如:hessian。

       hessian它的一个最大特色就是跨语言,hessian提供了一整套的byte[]的写入规范。这样其他语言在实现hessian序列化的时候就可以参照这套的标准规范,从而达到不同语言之间的兼容效果,因此hessian的序列化都是围绕这byte数组来的。

 

四、Java序列化实操       

      接下来 为师 将安排2号技师 给大家来个激情的表演,请大家准备好纸巾。

       2号技师要表演什么呢?她要表演将一个MeiMei对象,写到文件中,然后在成功的掏出来。

       2号技师要开始了,大家准备好了没有。

环境说明:

(1)JDK: 1.8

(2)OS:MAC

 

4.1 准备一个美眉

       我们先准备一个美眉类,MeiMei.java:

package com.kfit;

import java.io.Serializable;

public class MeiMei implements Serializable{
    private static final long serialVersionUID = 1L;
    private long id;
    private String name;//美眉的名称
    private String cup;//你懂得

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCup() {
        return cup;
    }

    public void setCup(String cup) {
        this.cup = cup;
    }

    @Override
    public String toString() {
        return "MeiMei [id=" + id + ", name=" + name + ", cup=" + cup + "]";
    }
}

BTW:这个类没啥特殊之处,就是实现了接口Serializable。

 

4.2 开始表演

     2号技师开始你的表演吧,舞台已经为你准备好了,这时候,只见2号技师,缓缓走上舞台,用她婀娜多姿的身材,开始了她精彩的表演......(想想都有点小激动٩(๑>◡<๑)۶)

package com.kfit;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        /*
         * 序列化.
         */
        MeiMei mm = new MeiMei();
        mm.setId(2);
        mm.setName("2号技师");
        mm.setCup("36C");

        ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(new File("/data/tmp/meimei.out")));
        output.writeObject(mm);
        output.close();
        System.out.println("序列化成功:"+mm);

        /*
         * 反序列化
         */
        ObjectInputStream input = new ObjectInputStream(new FileInputStream(new File("/data/tmp/meimei.out")));
        MeiMei mm2 = (MeiMei) input.readObject();
        input.close();
        System.out.println("反序列化成功:"+mm2);
    }

}    

BTW:

(1)这里关注Main方法,就是将对象序列化和反序列化。

(2)ObjectOutputStream代表对象输出流,ObjectInputStream代表对象输入流。

(3)流执行之后记得close掉。

       Run一下吧,观察控制台输出如下信息:

序列化成功:MeiMei [id=2,name=2号技师, cup=36C]

反序列化成功:MeiMei[id=2, name=2号技师, cup=36C]

       如果将MeiMei的implementsSerializable去掉就会报如下错:

Exception inthread "main" java.io.NotSerializableException: com.kfit.MeiMei

       at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)

       atjava.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

       at com.kfit.Test.main(Test.java:22)

       看,技师的衣服不能随便脱吧,这可闹大了。

 

4.3 扒开衣服看看

       2号技师慢慢的将衣服….此处省略一万字。

       Java对象写到文件之后,到底是什么东东呐,扒开meimei.out看下。

       啥,你不知道怎么扒开,给你一个女的,你还不会了。

       在Mac下可以使用Hex Fiend打开二进制文件,下载地址:

https://github.com/ridiculousfish/HexFiend/releases 

       另外一种简单的方式就是使用vi进行打开(这是Mac哦),主要两步操作(相信玩Mac的都能看得懂):

(1)首先以二进制方式编辑这个文件

vi -b /data/tmp/meimei.out

(2)使用xxd转换为16进制

:%!xxd

       扒开之后,可以看到如下文本:

色谈Java序列化:女孩子慎入 - 第280篇_java_02

 

       怎么样,好看吗,看不懂,看不懂就对了,衣服又不是你脱的,你怎么能看明白呐,这种事情还得自己来。

       这就是字节,字节可以保存到文件中,也可以保存到数据库中,取出来字节之后,就可以在转换为原来的数据信息了。

 

4.4 serialVersionUID

       

     美女,serialVersionUID这是啥,你干嘛带着,这是我的唯一标识,要脱衣服,就得有serialVersionUID,不然不让脱哦。

       这个serialVersionUID有啥用呐:来验证版本一致性的。在反序列化时,jvm会把传来的字节流中的serialVersionUID和本地相应实体类的serialVersionUID进行比较,如果相同就认为一致,可以进行反序列化,否则出现InvalidCastException异常。

       怎么来验证下是不是2号技师呐?只需要将脱衣服的代码删除即可:

//        MeiMei mm = new MeiMei();
//        mm.setId(2);
//        mm.setName("2号技师");
//        mm.setCup("36C");
//        
//        ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(new File("/data/tmp/meimei.out")));
//        output.writeObject(mm);
//        output.close();
//        System.out.println("序列化成功:"+mm);

 

BTW:将这段序列化的代码注释掉。(前提是已经执行成功过一次,已经有meimei.out文件了)

       此时执行剩下的代码(也就是反序列化代码),会发现此时还是可以匹配到2号技师的,只见控制台打印如下信息:

反序列化成功:MeiMei[id=2, name=2号技师, cup=36C]

       紧接着将MeiMei类中的serialVersionUID修改为2L。

       这时候在摸一下,哦,不,口误是run一下,完了,2号技师丢了:

Exception in thread "main" java.io.InvalidClassException: com.kfit.MeiMei;
 local class incompatible: stream classdesc serialVersionUID = 1, 
local class serialVersionUID = 2

BTW:由于serialVersionUID不一致,所以匹配失败,对象反序列化也失败了。

 

4.5 这部分不让摸:static

       「内心世界:不敢写了,在写就要变成 黄色编程 了,聪明如你,自己领悟吧」

static静态变量不是对象状态的一部分,因此它不参与序列化。那么反序列化之后。我们可以这么理解,静态变量的话,不属于对象的特有的,谁都可以进行改变,所以序列化了之后,在反序列返回来可能就不是那个值了。

 

4.6 加钱就让:static-final  

色谈Java序列化:女孩子慎入 - 第280篇_java_03    

 如果一个变量修饰为static final的话,这时候由于final定义的不可被改变,那么这时候这个属性就会被持久化了。

 

4.7 transient:定义临时变量

       transient关键字的作用,简单地说,就是让某些被修饰的成员属性变量不被序列化。我们可以在MeiMei类添加一个属性:

private transient int age;

       那么在序列化的设置age为18,这时候在运行:

序列化成功:MeiMei [id=2,name=2号技师, cup=36C, age=18]

反序列化成功:MeiMei[id=2, name=2号技师, cup=36C, age=0]

       看到没有age并没有被序列化,因为反序列回来之后age为0了。

 

五、悟纤小结

       今天师傅也讲了不少东西了,不知道悟纤你听懂了多少,你来给大家总结下吧:

(1)序列化和反序列化的定义:就是对象转换为字节和字节转换为对象的过程。

(2)为什么需要序列化:主要是为了跨进程跨网络的数据传输。

(3)序列化的方式:常用的就是实现接口Serializable。

(4)如何让属性不序列化:只要将属性定义为transient即可。

       好,不错,总结的差不多。今天能记住这些就够了,好了悟纤,继续赶路吧,西天还远着呐。

色谈Java序列化:女孩子慎入 - 第280篇_java_04

白龙马蹄朝西,驮着唐三藏跟着仨徒弟。

西天取经上大路,一走就是几万里。

白龙马脖铃儿急,颠簸唐玄奘小跑仨兄弟,

西天取经不容易,容易干不成大业绩。

我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。

à悟空学院:http://t.cn/Rg3fKJD

 

SpringBoot视频:http://t.cn/R3QepWG

Spring Cloud视频:http://t.cn/R3QeRZc

SpringBoot Shiro视频:http://t.cn/R3QDMbh

SpringBoot交流平台:http://t.cn/R3QDhU0

SpringData和JPA视频:http://t.cn/R1pSojf

SpringSecurity5.0视频:http://t.cn/EwlLjHh

Sharding-JDBC分库分表实战:http://t.cn/E4lpD6e