分布式架构基础-序列化和反序列化

1.序列化的意义
2.java原生序列化
3.serialVersionUID 的作用
4.静态变量序列化
5.父类的序列化
6.Transient 关键字
7.绕开 transient 机制的办法
8.序列化的存储规则
9.序列化实现深克隆
10.分布式架构下常见序列化技术

序列化的意义

java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当 JVM处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象,Java 对象序列化就能够帮助我们实现该功能。
简单来说,序列化是把对象的状态信息转化为可存储或传输的形式,也就是把对象转化为字节序列的过程称为对象的序列化。反序列化是序列化的逆向过程,把字节数组反序列化为对象,把字节序列恢复为对象的过程称为对象的反序列化。


image.png
java原生序列化

前面的代码中演示了,如何通过 JDK 提供了 Java 对象的序列化方式实现对象序列化传输,主要通过输出流java.io.ObjectOutputStream和对象输入流java.io.ObjectInputStream来实现。

java.io.ObjectOutputStream:表示对象输出流 , 它的 writeObject(Object obj)方法可以对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream:表示对象输入流 ,它的 readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回
需要注意的是,被序列化的对象需要实现 java.io.Serializable 接口

例:基于 socket 进行对象传输

public class User implements Serializable {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
public class SocketServerProvider {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=null;
        BufferedReader in=null;
        try{
            serverSocket=new ServerSocket(8080);
            Socket socket=serverSocket.accept();
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            User user=(User)objectInputStream.readObject();
            System.out.println(user);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(in!=null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(serverSocket!=null){
                serverSocket.close();
            }
        }
    }
}
public class SocketClientConsumer {
    public static void main(String[] args) {
        Socket socket=null;
        ObjectOutputStream out=null;
        try {
            socket=new Socket("127.0.0.1",8080);
            User user=new User();
            out=new ObjectOutputStream(socket.getOutputStream());
            out.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(out!=null){
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(socket!=null){
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

serialVersionUID 的作用

Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
如果没有为指定的class配置serialVersionUID,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件有任何改动,得到的UID就会截然不同,可以保证在这么多类中,这个编号是唯一的。
serialVersionUID有两种显示的生成方式:
一是默认的1L,比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。
当实现java.io.Serializable接口的类没有显式地定义一个serialVersionUID变量的时候,Java序列化机制会根据编译的Class自动生成一个serialVersionUID当作序列化版本来比较用,这种情况下,如果Class文件(类名,方法名等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化。

静态变量序列化

在User中添加一个全局的静态变量num , 在执行序列化以后修改num的值为10, 然后通过反序列化以后得到的对象去输出num的值。

public class User implements Serializable{
    private String name;
    private int age;
    public static int num=5;//设置静态变量num值为5
...
...
...
}

public class Test {

    public static void main(String[] args) {
        ISerializer serializer=new JavaSerializer();
        User user=new User();
        user.setName("taofut");
        user.setAge(27);
        byte[] bytes=serializer.serializer(user);
        //序列化以后,将num值改为10
        User.num=10;

        User user1=serializer.deSerializer(bytes,User.class);
        System.out.println(user1+"--"+User.num);
    }
    //执行结果:User{name='taofut', age=27}--10
}

最后的输出是10,我们可能会觉得,10是在序列化之后修改的,按理说反序列化应该输出的是5才对。理论上打印的num是从读取的对象里获得的,应该是保存时的状态才对。之所以打印10的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。

父类的序列化

一个子类实现了Serializable接口,而它的父类却没有实现Serializable接口,在子类中设置父类的成员变量的值,接着序列化该子类对象。再反序列化出来以后输出父类属性的值。结果应该是什么?

public class SuperUser {
    String sex;

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }
}

public class Test {

    public static void main(String[] args) {
        ISerializer serializer=new JavaSerializer();
        User user=new User();
        user.setName("taofut");
        user.setAge(27);
        user.setSex("男");
        byte[] bytes=serializer.serializer(user);
        User.num=10;

        User user1=serializer.deSerializer(bytes,User.class);
        System.out.println(user1+"--"+user1.getSex());
    }
    //执行结果:User{name='taofut', age=27}--null
}

最终结果发现,父类的sex字段的值为null,也就是说父类没有实现序列化。
结论:
1》 当一个父类没有实现序列化时,子类继承该父类并且实现了序列化。在反序列化该子类后,是没办法获取到父类的属性值的。
2》当一个父类实现序列化,子类自动实现序列化,不需要再显示实现Serializable接口。
3》当一个对象的实例变量引用了其他对象,序列化该对象时也会把引用对象进行序列化,但是前提是该引用对象必须实现序列化接口。

Transient关键字

Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如 int型的是0,对象型的是null。

public class UserTwo extends SuperUser implements Serializable{
    private String name;
    private int age;
    public static int num=5;

    private transient String address;
    ...
    ...
    ...
}

public class Test {

    public static void main(String[] args) {
        ISerializer serializer=new JavaSerializer();
        UserTwo user=new UserTwo();
        user.setName("taofut");
        user.setAge(27);
        user.setSex("男");
        user.setAddress("浙江省");//该字段被transient修饰过
        byte[] bytes=serializer.serializer(user);

        UserTwo user1=serializer.deSerializer(bytes,UserTwo.class);
        System.out.println(user1+"--"+user1.getAddress());
    }
    //执行结果:User{name='taofut', age=27}--null
}
绕开transient机制的办法
public class UserTwo extends SuperUser implements Serializable{
    private String name;
    private int age;
    public static int num=5;

    private transient String address;
    //序列化对象
    private void writeObject(ObjectOutputStream objectOutputStream) throws IOException {
        objectOutputStream.defaultWriteObject();
        objectOutputStream.writeObject(address);
    }
    //反序列化
    private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        objectInputStream.defaultReadObject();
        address=(String)objectInputStream.readObject();
    }
    ...
    ...
    ...
}

public class Test {

    public static void main(String[] args) {
        ISerializer serializer=new JavaSerializer();
        UserTwo user=new UserTwo();
        user.setName("taofut");
        user.setAge(27);
        user.setSex("男");
        user.setAddress("浙江省");
        byte[] bytes=serializer.serializer(user);


        UserTwo user1=serializer.deSerializer(bytes,UserTwo.class);
        System.out.println(user1+"--"+user1.getAddress());
    }
    //执行结果:User{name='taofut', age=27}--浙江省
}

以上代码可能会产生一个疑问:writeObject和readObject这两个私有的方法,既不属于 Object、也不是Serializable,为什么能够在序列化的时候被调用呢? 原因是,ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,(反射可以绕开权限限制)所以这些方法必须声明为priate,以至于供ObjectOutputStream来使用。
对希望采用自定义序列化的字段用transient修饰,然后在先调用writeObject和readObject方法中对transient修饰的字段进行序列化,并在方法最开始调用defaultReadObject和defaultReadObject方法,对其他字段采用默认序列化方式。这样的好处是方便兼容。
被transient修饰的成员,只是不能被默认的序列化方法序列化(从源码中也可以看到),但却可以被自定义的序列化方法序列化。

序列化的存储规则
public class StoreRuleDemo {

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

        ObjectOutputStream outputStream=new ObjectOutputStream(new FileOutputStream(new File("user")));
        User user=new User();
        user.setName("taofut");
        user.setAge(27);
        user.setSex("男");
        outputStream.flush();
        outputStream.writeObject(user);
        System.out.println(new File("user").length());

        outputStream.writeObject(user);
        outputStream.flush();
        outputStream.close();
        System.out.println(new File("user").length());
    }
    //执行结果:
    //89
    //94
}

我们发现,同一对象两次写入文件,打印出写入一次对象后的存储大小和写入两次后的存储大小,第二次写入对象时文件只增加了5个字节。
这是因为,Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的5个字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,该存储规则极大的节省了存储空间。

序列化实现深克隆

1》浅克隆机制:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。

public class CloneDemo {

    public static void main(String[] args) throws CloneNotSupportedException {
        Email email=new Email();
        email.setContent("今天晚上6点开会");
        Person p1=new Person("taofut");
        p1.setEmail(email);

        Person p2=p1.clone();
        p2.setName("fut");
        p2.getEmail().setContent("今天晚上6点半开会");
        System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
        System.out.println(p2.getName()+"->"+p2.getEmail().getContent());
    }
    //执行结果:
    //taofut->今天晚上6点半开会
    //fut->今天晚上6点半开会
}

以上案例很好的说明了,浅克隆不能复制新的引用,Email引用还是指向的同一个对象,这就导致了都是”今天晚上6点半开会”的结果出现。

2》深克隆机制:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

public class Email implements Serializable{
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

public class Person implements Cloneable,Serializable{
    private String name;
    private Email email;

    public String getName() {
        return name;
    }

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

    public Email getEmail() {
        return email;
    }

    public void setEmail(Email email) {
        this.email = email;
    }

    @Override
    protected Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }

    public Person(String name) {
        this.name = name;
    }
    //深克隆方法
    public Person deepClone() throws IOException,ClassNotFoundException{
        ByteArrayOutputStream bos=new ByteArrayOutputStream();
        ObjectOutputStream outputStream=
                new ObjectOutputStream(bos);
        outputStream.writeObject(this);

        ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream inputStream=new ObjectInputStream(bis);
        return (Person)inputStream.readObject();
    }
}

public class DeepDemo {

    public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
        Email email=new Email();
        email.setContent("今天晚上6点开会");
        Person p1=new Person("taofut");
        p1.setEmail(email);

        Person p2=p1.deepClone();
        p2.setName("fut");
        p2.getEmail().setContent("今天晚上6点半开会");
        System.out.println(p1.getName()+"->"+p1.getEmail().getContent());
        System.out.println(p2.getName()+"->"+p2.getEmail().getContent());
    }
    //执行结果:
    //taofut->今天晚上6点开会
    //fut->今天晚上6点半开会
}

由于Java本身提供的序列化机制存在两个问题

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

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,864评论 0 24
  • Valentine 转载请标明出处。 序列化的意义 Java 平台允许我们在内存中创建可复用的Java 对象,但一...
    valentine_liang阅读 886评论 0 0
  • 一、 序列化和反序列化概念 Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化de...
    步积阅读 1,443评论 0 10
  • 官方文档理解 要使类的成员变量可以序列化和反序列化,必须实现Serializable接口。任何可序列化类的子类都是...
    狮_子歌歌阅读 2,411评论 1 3
  • 在夜店门外,美丽姑娘 他发现你长得似一串最贵最甜的葡萄 或者那串最漂亮的葡萄像你寄世的胴体 他说他可以爱你 可不像...
    钱方军阅读 394评论 3 14